# Inset panel

> A tinted panel that sets a note, warning, tip or key figure apart inside a page. Five tones, an optional icon hung level with the first line above a thin rail, an optional title, and three padding presets.

- ID: `cmp_inset_panel_01`
- Slug: `inset-panel-01`
- Version: `1.0.0` (current)
- Status: published
- Published: 2026-10-01
- Updated: 2026-10-01
- Available versions: `1.0.0`
- Kind: control
- Primary category: `layout`
- Detail page: https://pagesugar.com/components/inset-panel-01
- Preview: https://pagesugar.com/preview/inset-panel-01

## Variants

| Variant | Label | Default | Artifact digest |
| --- | --- | --- | --- |
| `neutral` | Neutral | yes | `sha256-0716cef44860e96212cd921c26fb82bc1f1604d255ae9753d4a0ba89daf4561b` |
| `blue` | Blue accent | no | `sha256-19a2a57052c73333f7af16c68626536e1242908b493e206a73920ce49d12f042` |

## Runtime and compatibility

- Runtime: svelte
- Svelte: 5
- SvelteKit required: no (portable Svelte component)
- Tailwind CSS: 4
- SSR: supported
- Requires client-side JavaScript: no
- Integration level: presentational
- Appearance modes: light
- Suggested directory: `src/lib/components/inset-panel-01`

## Dependencies

No third-party runtime packages.

## Services

No external services required.

## Usage

Presentational only: the panel frames the title and body you pass and does nothing else. It is static content, not an alert: no live region, no dismiss button, no timeout. Tone is shown by colour and the icon, so put the meaning in words as well, for example "Note:" or "Warning:" at the start of the title.

Required props: none

```svelte
<script lang="ts">
	import InsetPanel from '$lib/components/inset-panel-01/InsetPanel.svelte';
</script>

<InsetPanel tone="info" title="Note: guests don't take a seat" as="aside">
	{#snippet icon()}
		<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round">
			<circle cx="8" cy="8" r="6.25" />
			<path d="M8 7.25v3.5M8 5.25h.01" />
		</svg>
	{/snippet}
	<p>Guests are free on Team and Business, and they only see the boards you share.</p>
</InsetPanel>
```

Limitations:

- Not a dismissible alert or toast, and not a live region: content inserted after load is not announced. Use an alert component for messages that appear in response to an action.
- Tone is not conveyed by colour alone only if your copy says it: start the title (or the body, with no title) with a word such as Note, Tip or Warning.
- The panel fills the width of its parent; set the measure on the column it sits in.
- The rail starts at the first line's cap height using the CSS cap unit; browsers without it (Chrome before 118, Safari before 17.2) start the rail at the top of the line.
- Light appearance by default. The tokens retone it for a dark or tinted page, but no dark mode is declared or selected automatically.

## Usage guide

### Inset panel

A tinted panel for the note, tip, warning or key figure that has to stand apart from the copy
around it. It frames whatever you pass and does nothing else: no dismiss button, no live region.

#### Say the tone in words

The icon and the colour show the tone to people who can see them. Everyone else reads the title,
so start it with the word that carries the meaning:

```svelte
<InsetPanel tone="warning" title="Warning: archiving removes guest access" as="aside">
	{#snippet icon()}<TriangleIcon />{/snippet}
	<p>Restore the board from Archive within 30 days to give guests access again.</p>
</InsetPanel>
```

With no title, start the body with the word instead (`<p><strong>Note:</strong> …</p>`).

#### Which element

- `div` (default): part of the flow, no landmark.
- `aside`: tangential content a reader could skip, such as a tip. Named by the title.
- `section`: a pull-out that is a part of the page in its own right, such as a plan summary.
  Named by the title; give it a `titleLevel` so it sits in the outline.

#### Retoning for a dark page

```css
.dark-band {
	--inset-panel-surface: #18181b;
	--inset-panel-ink: #fafafa;
	--inset-panel-muted: #a1a1aa;
	--inset-panel-accent: #fafafa;
	--inset-panel-info: #38bdf8;
	--inset-panel-success: #4ade80;
	--inset-panel-warning: #fbbf24;
}
```

The fills are mixed from each tone into `--inset-panel-surface`, so they darken with the page.
On a cream or tinted page, set `--inset-panel-surface` to the page colour for the same reason.

## Props

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `children` | `Snippet` | no |  | Body content. Paragraphs, lists, links, strong and code pick up the panel's body type from rules in the components cascade layer, 12 px apart. Omit it for a title-only panel. |
| `tone` | 'neutral' \| 'info' \| 'success' \| 'warning' \| 'accent' | no | `'neutral'` | Colours the icon, the rail and list markers. Info, success and warning also tint the fill; neutral and accent keep the neutral fill. |
| `title` | `string` | no |  | Title above the body. Start it with a word that says the tone (Note, Tip, Warning). On aside and section it names the landmark. |
| `titleLevel` | 2 \| 3 \| 4 \| null | no | `null` | Renders the title as h2, h3 or h4 to fit your page outline, or as a plain paragraph with null. |
| `icon` | `Snippet` | no |  | Decorative icon, hidden from assistive technology and drawn in the tone colour in a box one line tall beside the first line. An svg is sized to 16 px (20 px at lg padding). |
| `as` | 'div' \| 'aside' \| 'section' | no | `'div'` | Root element. aside for tangential content such as a tip; section for a pull-out that is a part of the page. Both get aria-labelledby the title when there is one. |
| `padding` | 'sm' \| 'md' \| 'lg' | no | `'md'` | Internal spacing of 12, 16 or 24 px. Title and body step from 14/14 to 16/14 to 18/16 px. |

## Customization

Pass your own title, icon and body, retone the panel through eight --inset-panel-\* variables, and edit the source for spacing or layout.

- Content: everything in the body is yours. Plain p, ul, ol, a, strong and code get the panel's type from rules in the components cascade layer, so a Tailwind utility on your own markup wins.
- Tone words: the title carries the meaning for people who cannot see the colour. "Note: guests don't take a seat" works; a colour alone does not.
- Accent: --inset-panel-accent colours the accent tone's icon and rail and the focus ring on links in the body. Set it to your brand colour; the fill stays neutral.
- Meaning colours: --inset-panel-info (#0369a1), --inset-panel-success (#15803d) and --inset-panel-warning (#b45309) colour each tone's icon, rail and fill tint. Keep them at 3:1 or better on the fill for the icon.
- Text: --inset-panel-ink sets the title, links and strong text; --inset-panel-muted sets the body and the neutral tone's icon and rail. Keep both at 4.5:1 on every fill.
- Surface: --inset-panel-surface is the page colour the tints are mixed into (white by default). On a cream page, set it to the page colour and every tone's fill follows.
- Radius: --inset-panel-radius sets the corners (12 px).
- Dark page retone: surface #18181b, ink #fafafa, muted #a1a1aa, accent #fafafa, info #38bdf8, success #4ade80, warning #fbbf24 (all --inset-panel-\*). usage.md has the snippet.
- Spacing: padding sm, md and lg are 12, 16 and 24 px; put margins on the wrapper or the column around the panel.

| Token | Public CSS variable |
| --- | --- |
| `accent` | `--inset-panel-accent` |
| `ink` | `--inset-panel-ink` |
| `muted` | `--inset-panel-muted` |
| `surface` | `--inset-panel-surface` |
| `info` | `--inset-panel-info` |
| `success` | `--inset-panel-success` |
| `warning` | `--inset-panel-warning` |
| `radius` | `--inset-panel-radius` |

## Accessibility

- With as="aside" or as="section" and a title, the panel is a landmark named by its title through aria-labelledby. As a div, or with no title, it gets no name and no landmark is announced by name.
- The panel is static content, not a live region: it has no role="alert" or role="status", so screen readers read it in place rather than interrupting.
- The icon and the rail are aria-hidden. Tone is shown by colour and shape only, so the copy must name it: start the title with Note, Tip or Warning. In the Arabic or another translation, translate that word too.
- The title level is yours: titleLevel 2, 3 or 4 puts it in the page outline; null keeps it a paragraph.
- Default token contrast (WCAG relative luminance): ink #18181b and muted #52525b clear 4.5:1 on every tone's fill, the weakest being muted on the warning fill at about 7:1. The tone colours (#0369a1, #15803d, #b45309, #52525b, #18181b, blue palette #1d4ed8) clear 4.5:1 on their fills, so the icon passes 3:1 with room.
- Links in the body keep an underline at rest and show a two-pixel accent focus ring with a two-pixel offset on keyboard focus.
- The layout uses logical properties: under dir="rtl" the icon and rail move to the right and the title's letter-spacing resets to 0. In forced-colours mode the panel gets a system-colour outline and the rail stays visible.

Known limitations:

- Contrast is computed for the default tokens; re-check any token you change (4.5:1 for text, 3:1 for the icon).
- The fill is a faint tint with no outline; on a page coloured close to it, set --inset-panel-surface to the page colour so the tint is mixed against the right background.

## License

- Declared source: MIT
- Default license approval is pending. See https://pagesugar.com/docs/license.

## Source

- Palette: Neutral (`neutral`)
- Entry: `InsetPanel.svelte`
- Suggested directory: `src/lib/components/inset-panel-01`
- Files: 1
- Artifact digest: `sha256-0716cef44860e96212cd921c26fb82bc1f1604d255ae9753d4a0ba89daf4561b`

Paths below are relative to the suggested directory. Copy the files as they are;
they import nothing from this site.

#### `InsetPanel.svelte`

Role: entry · 8886 bytes · SHA-256 `01cd4696387b0e7e1e1122a6a733433b18a3949dc6160a819146910acd1be644`

```svelte
<script module lang="ts">
	/** Surface tone. Info, success and warning carry meaning; accent follows your brand colour. */
	export type InsetPanelTone = 'neutral' | 'info' | 'success' | 'warning' | 'accent';
	/** Internal spacing of 12, 16 or 24 px. The type steps with it. */
	export type InsetPanelPadding = 'sm' | 'md' | 'lg';
</script>

<script lang="ts">
	import type { Snippet } from 'svelte';

	interface Props {
		/** Body content: paragraphs, lists, links, a figure. */
		children?: Snippet;
		/** Surface tone. Put the meaning in words too, such as "Note:" or "Warning:" in the title. */
		tone?: InsetPanelTone;
		/** Panel title, set above the body. */
		title?: string;
		/** Heading level for the title, or null for a plain paragraph that stays out of the outline. */
		titleLevel?: 2 | 3 | 4 | null;
		/** Decorative icon, hung in the start gutter level with the first line. */
		icon?: Snippet;
		/** Root element. Use aside for tangential content; aside and section are named by the title. */
		as?: 'div' | 'aside' | 'section';
		padding?: InsetPanelPadding;
	}

	let {
		children,
		tone = 'neutral',
		title,
		titleLevel = null,
		icon,
		as = 'div',
		padding = 'md'
	}: Props = $props();

	const uid = $props.id();
	const titleId = `${uid}-title`;

	/* A landmark is named only when it is one and there is a title to name it with. */
	const labelledBy = $derived(title && as !== 'div' ? titleId : undefined);
	const titleTag = $derived(titleLevel ? `h${titleLevel}` : 'p');

	const tones: Record<InsetPanelTone, string> = {
		neutral: 'inset-panel--neutral',
		info: 'inset-panel--info',
		success: 'inset-panel--success',
		warning: 'inset-panel--warning',
		accent: 'inset-panel--accent'
	};
	const paddings: Record<InsetPanelPadding, string> = {
		sm: 'inset-panel--sm p-3',
		md: 'inset-panel--md p-4',
		lg: 'inset-panel--lg p-6'
	};
</script>

<svelte:element
	this={as}
	class={[
		'inset-panel grid min-w-0 grid-cols-[auto_minmax(0,1fr)] rounded-(--_radius) bg-(--_fill) text-start text-(length:--_body-size)/(--_lh) text-(--_muted) forced-colors:outline-1 forced-colors:outline-[CanvasText] forced-colors:outline-solid',
		tones[tone],
		paddings[padding],
		icon ? 'gap-x-3' : 'gap-x-4'
	]}
	aria-labelledby={labelledBy}
>
	<!--
		Forced colours drop the fill, so the root gains a system outline there.
		The start gutter: the icon sits in a box one line tall, level with the first line, and the
		rail runs on from it to the last line. Both are logical, so they move to the right in RTL.
		The rail stays inside the padding on purpose: an inset rule, not a strip on the panel's edge.
		It is positioned rather than stacked, so it never adds height: a one-line panel stays centred.
	-->
	<div
		class={['inset-panel__gutter relative flex flex-col items-center', !icon && 'w-px']}
		aria-hidden="true"
	>
		{#if icon}
			<span
				class="inset-panel__icon flex h-(--_lh) w-(--_icon) shrink-0 items-center justify-center text-(--_tone)"
			>
				{@render icon()}
			</span>
		{/if}
		<span
			class={[
				'inset-panel__rail absolute bottom-(--_rail-inset) w-px rounded-full bg-(--_rail) forced-colors:bg-[CanvasText] forced-colors:forced-color-adjust-none',
				icon
					? 'top-(--_rail-icon-inset)'
					: title
						? 'top-(--_rail-title-inset)'
						: 'top-(--_rail-inset)'
			]}
		></span>
	</div>

	<div class="inset-panel__content min-w-0 break-words">
		{#if title}
			<svelte:element
				this={titleTag}
				id={titleId}
				class="inset-panel__title text-(length:--_title-size)/(--_lh) font-semibold tracking-[-0.011em] text-balance text-(--_ink)"
			>
				{title}
			</svelte:element>
		{/if}
		{#if children}
			<div class={['inset-panel__body text-pretty *:not-first:mt-3', title && 'mt-2']}>
				{@render children()}
			</div>
		{/if}
	</div>
</svelte:element>

<style>
	/* Public tokens: set --inset-panel-* on the panel or any ancestor to retone it. */
	.inset-panel {
		--_accent: var(--inset-panel-accent, #18181b);
		--_ink: var(--inset-panel-ink, #18181b);
		--_muted: var(--inset-panel-muted, #52525b);
		--_surface: var(--inset-panel-surface, #ffffff);
		--_info: var(--inset-panel-info, #0369a1);
		--_success: var(--inset-panel-success, #15803d);
		--_warning: var(--inset-panel-warning, #b45309);
		--_radius: var(--inset-panel-radius, 12px);

		/* Formulas, read by utilities in the markup. Each tone sets --_tone and --_fill below. */
		--_rail: color-mix(in oklab, var(--_tone) 45%, transparent);
		/*
		 * The rail starts at the first line's cap height and ends on the last baseline. 1cap is the
		 * body's; beside a title it is scaled up to the title's size.
		 */
		--_rail-inset: calc((var(--_lh) - 1cap) / 2);
		--_rail-title-inset: calc((var(--_lh) - 1cap * var(--_title-scale)) / 2);
		/* Under an icon it starts 4 px below the icon's line box; with no second line it has no length. */
		--_rail-icon-inset: calc(var(--_lh) + 0.25rem);
	}

	/*
	 * Tones. Neutral and accent keep the neutral fill: the accent marks the rail and icon, never the
	 * surface (DESIGN §3.2). Meaning tones mix a few percent of their colour into the page surface.
	 */
	.inset-panel--neutral {
		--_tone: var(--_muted);
		--_fill: color-mix(in oklab, var(--_ink) 4%, var(--_surface));
	}
	.inset-panel--accent {
		--_tone: var(--_accent);
		--_fill: color-mix(in oklab, var(--_ink) 4%, var(--_surface));
	}
	.inset-panel--info {
		--_tone: var(--_info);
		--_fill: color-mix(in oklab, var(--_info) 6%, var(--_surface));
	}
	.inset-panel--success {
		--_tone: var(--_success);
		--_fill: color-mix(in oklab, var(--_success) 6%, var(--_surface));
	}
	.inset-panel--warning {
		--_tone: var(--_warning);
		--_fill: color-mix(in oklab, var(--_warning) 7%, var(--_surface));
	}

	/* Padding presets step the type and the one-line box the icon and rail measure from. */
	.inset-panel--sm {
		--_title-scale: 1;
		--_title-size: 0.875rem;
		--_body-size: 0.875rem;
		--_lh: 1.25rem;
		--_icon: 1rem;
	}
	.inset-panel--md {
		--_title-scale: 1.142857;
		--_title-size: 1rem;
		--_body-size: 0.875rem;
		--_lh: 1.375rem;
		--_icon: 1rem;
	}
	.inset-panel--lg {
		--_title-scale: 1.125;
		--_title-size: 1.125rem;
		--_body-size: 1rem;
		--_lh: 1.5rem;
		--_icon: 1.25rem;
	}

	/* Whatever the icon snippet renders fills the box at the preset size: content the panel does not render. */
	.inset-panel__icon > :global(svg) {
		width: var(--_icon);
		height: var(--_icon);
	}
	/*
	 * A 16-unit icon drawn at 20 px would thicken its 1.75 stroke to 2.2 px. At lg it is set back to
	 * 1.5 px (DESIGN §3.7); a stroke-width you put on the paths themselves still wins.
	 */
	.inset-panel--lg .inset-panel__icon > :global(svg) {
		stroke-width: 1.2;
	}

	/*
	 * Defaults for the body markup you pass in. They sit in the components layer, so any Tailwind
	 * utility you put on your own markup wins over them.
	 */
	@layer components {
		.inset-panel__body :global(:where(p, ul, ol, figure, blockquote)) {
			margin-block: 0;
		}
		.inset-panel__body :global(:where(ul, ol)) {
			padding-inline-start: 1.25em;
		}
		.inset-panel__body :global(:where(ul)) {
			list-style-type: disc;
		}
		.inset-panel__body :global(:where(ol)) {
			list-style-type: decimal;
		}
		.inset-panel__body :global(:where(li + li)) {
			margin-block-start: 0.25rem;
		}
		.inset-panel__body :global(:where(li)::marker) {
			color: var(--_tone);
		}
		.inset-panel__body :global(:where(ol > li)::marker) {
			font-variant-numeric: tabular-nums;
		}
		.inset-panel__body :global(:where(strong, b)) {
			font-weight: 600;
			color: var(--_ink);
		}
		/* Links inside the panel are prose, so they keep an underline at rest. */
		.inset-panel__body :global(:where(a)) {
			color: var(--_ink);
			text-decoration-line: underline;
			text-decoration-thickness: 1px;
			text-underline-offset: 3px;
			text-decoration-color: color-mix(in oklab, var(--_ink) 35%, transparent);
			transition: text-decoration-color 150ms cubic-bezier(0.2, 0, 0, 1);
		}
		@media (hover: hover) {
			.inset-panel__body :global(:where(a):hover) {
				text-decoration-color: var(--_ink);
			}
		}
		.inset-panel__body :global(:where(a):focus-visible) {
			outline: 2px solid var(--_accent);
			outline-offset: 2px;
			border-radius: 2px;
		}
		.inset-panel__body :global(:where(code)) {
			font-family: var(--font-mono, ui-monospace, monospace);
			font-size: 0.875em;
			color: var(--_ink);
		}
		@media (prefers-reduced-motion: reduce) {
			.inset-panel__body :global(:where(a)) {
				transition: none;
			}
		}
	}

	/*
	 * Other scripts, in one place: the title's tracking resets for right-to-left text, and Japanese
	 * and Chinese break at phrases, not mid-word.
	 */
	.inset-panel__title:dir(rtl) {
		letter-spacing: 0;
	}
	.inset-panel:lang(ja) :is(.inset-panel__title, .inset-panel__body) {
		word-break: auto-phrase;
	}
	.inset-panel:lang(zh) .inset-panel__title {
		word-break: keep-all;
	}
</style>
```

## Artifacts

### Neutral (`neutral`) (default)

- Artifact digest: `sha256-0716cef44860e96212cd921c26fb82bc1f1604d255ae9753d4a0ba89daf4561b`
- Entry: `InsetPanel.svelte`
- Receipt: https://pagesugar.com/artifacts/cmp_inset_panel_01/1.0.0/neutral/sha256-0716cef44860e96212cd921c26fb82bc1f1604d255ae9753d4a0ba89daf4561b/manifest.json
- Bundle: https://pagesugar.com/artifacts/cmp_inset_panel_01/1.0.0/neutral/sha256-0716cef44860e96212cd921c26fb82bc1f1604d255ae9753d4a0ba89daf4561b/bundle.zip (6297 bytes, sha256 `0d8de8c5d4193afd2ae864ed12b12a49f54b1634a95aceedcd3340c21b67227a`)

Files:

- `InsetPanel.svelte` (entry, 8886 bytes): https://pagesugar.com/artifacts/cmp_inset_panel_01/1.0.0/neutral/sha256-0716cef44860e96212cd921c26fb82bc1f1604d255ae9753d4a0ba89daf4561b/source/InsetPanel.svelte

### Blue accent (`blue`)

- Artifact digest: `sha256-19a2a57052c73333f7af16c68626536e1242908b493e206a73920ce49d12f042`
- Entry: `InsetPanel.svelte`
- Receipt: https://pagesugar.com/artifacts/cmp_inset_panel_01/1.0.0/blue/sha256-19a2a57052c73333f7af16c68626536e1242908b493e206a73920ce49d12f042/manifest.json
- Bundle: https://pagesugar.com/artifacts/cmp_inset_panel_01/1.0.0/blue/sha256-19a2a57052c73333f7af16c68626536e1242908b493e206a73920ce49d12f042/bundle.zip (6303 bytes, sha256 `594a057a43268d13d0401aedd569802c8af85144b420c452fff585aedeb6b21c`)

Files:

- `InsetPanel.svelte` (entry, 8886 bytes): https://pagesugar.com/artifacts/cmp_inset_panel_01/1.0.0/blue/sha256-19a2a57052c73333f7af16c68626536e1242908b493e206a73920ce49d12f042/source/InsetPanel.svelte
