Skip to content
Download ZIP

Neutral palette · 6.1 KB ZIP File receipt View as Markdown View code

Preview

Fit to the available width. The frame follows the height of its content; previews taller than the maximum auto-height scroll inside it.

Give this component to your coding agent Copy a prompt that fetches this exact version and palette through the PageSugar MCP server.
cmp_inset_panel_01 · version 1.0.0 · Neutral palette
Using the PageSugar MCP server, fetch component cmp_inset_panel_01 version 1.0.0 with variant "neutral", first inspect its requirements and license status and confirm this project uses Svelte 5 and Tailwind CSS 4. Retrieve every manifest file, including binary assets and any manifest-only response files, preserving relative paths. Then integrate the source and follow its usage notes. Run project checks, review the browser result and report anything unverified. Do not substitute another version or invent missing files.

Not connected yet? Set up the MCP server

Code

Palette
Neutral
Version
1.0.0
Digest
Full digest
sha256-0716cef44860e96212cd921c26fb82bc1f1604d255ae9753d4a0ba89daf4561b
InsetPanel.svelte Svelte · 8.7 KB Raw
<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>

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.

Suggested location
src/lib/components/inset-panel-01

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.

Example

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>

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 and content inputs#

On this page
NameTypeRequiredDefaultDescription
childrenSnippetNoNoneBody 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.
titlestringNoNoneTitle above the body. Start it with a word that says the tone (Note, Tip, Warning). On aside and section it names the landmark.
titleLevel2 | 3 | 4 | nullNonullRenders the title as h2, h3 or h4 to fit your page outline, or as a plain paragraph with null.
iconSnippetNoNoneDecorative 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#

On this page

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.

Public CSS variables

VariableToken
--inset-panel-accentaccent
--inset-panel-inkink
--inset-panel-mutedmuted
--inset-panel-surfacesurface
--inset-panel-infoinfo
--inset-panel-successsuccess
--inset-panel-warningwarning
--inset-panel-radiusradius

Accessibility#

On this page
  • 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.

Release details#

On this page
Integration
  • Presentational
  • Works without client-side JavaScript
  • Server-side rendering supported
Dependencies
No additional runtime packages beyond Svelte and Tailwind CSS
License

MIT. Default license approval is pending; see the license status before adopting the source.

Version history
  • 1.0.0 (Published) Current release · 1 October 2026

Only the current release is available. Keep downloaded source and its receipt if you need to use it again later.