Skip to content
Palette Default
Download ZIP

Default palette · 5.7 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_key_value_display_01 · version 1.0.0 · Default palette
Using the PageSugar MCP server, fetch component cmp_key_value_display_01 version 1.0.0 with variant "default", 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
Default
Version
1.0.0
Digest
Full digest
sha256-36ef959f9d1af722bf11ccd755e3d46efe50b45a640e4735aaf66fa91f94adf3
KeyValueList.svelte Svelte · 5.9 KB Raw
<script lang="ts" module>
	/** One label and its value. Extra fields ride along to the value snippet. */
	export interface KeyValueItem {
		/** What the value is: "Plan", "Billing period". */
		label: string;
		/** The value, already formatted. Missing, null or blank renders the empty marker. */
		value?: string | number | null;
		/** Makes the value a link. */
		href?: string;
		/** Set the value in the monospace face: hashes, IDs, codes. */
		mono?: boolean;
	}

	export type KeyValueLayout = 'stacked' | 'inline' | 'grid';
	export type KeyValueLabelWidth = 'narrow' | 'medium' | 'wide';
</script>

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

	interface Props {
		/** Rows in order. An empty array renders nothing. */
		items: T[];
		/** Label above value, label and value in two columns, or a multi-column grid of pairs. */
		layout?: KeyValueLayout;
		/** Hairline between rows (inline, stacked) or above each pair (grid). */
		dividers?: boolean;
		/** Width of the label column in the inline layout. */
		labelWidth?: KeyValueLabelWidth;
		/** Visible mark for a missing value. Hidden from assistive technology. */
		emptyValue?: string;
		/** What a screen reader hears for a missing value. */
		emptyLabel?: string;
		/** Custom value renderer for present values; call plain(item) for the default rendering. */
		value?: Snippet<[item: T, plain: Snippet<[KeyValueItem]>]>;
		/** Extra classes on the root, for margins or width. */
		class?: string;
	}

	let {
		items,
		layout = 'inline',
		dividers = true,
		labelWidth = 'medium',
		emptyValue = '—',
		emptyLabel = 'Not provided',
		value,
		class: className
	}: Props = $props();

	/*
	 * Every row shares the list's column tracks (subgrid in inline, a row pair in grid), so all
	 * values start on one edge and each label sits on its value's first baseline however either
	 * side wraps. The container query, not the viewport, decides when inline stacks, because the
	 * list usually sits in a panel or sidebar narrower than the page.
	 */
	const LAYOUTS: Record<KeyValueLayout, { list: string; row: string; rule: string }> = {
		stacked: {
			list: 'grid',
			row: 'grid gap-1',
			rule: 'border-b border-(--_hairline) last:border-b-0'
		},
		inline: {
			list: 'grid grid-cols-1 @md:grid-cols-[var(--_label-width)_minmax(0,1fr)] @md:gap-x-6',
			row: 'col-span-full grid grid-cols-subgrid gap-y-1 @md:items-baseline',
			rule: 'border-b border-(--_hairline) last:border-b-0'
		},
		grid: {
			list: 'grid grid-cols-1 gap-x-6 @md:grid-cols-2 @3xl:grid-cols-3',
			row: 'row-span-2 grid grid-rows-subgrid gap-y-1',
			rule: 'border-t border-(--_hairline)'
		}
	};

	const LABEL_WIDTHS: Record<KeyValueLabelWidth, string> = {
		narrow: '[--_label-width:7rem]',
		medium: '[--_label-width:10rem]',
		wide: '[--_label-width:14rem]'
	};

	const shape = $derived(LAYOUTS[layout] ?? LAYOUTS.inline);

	const missing = (item: KeyValueItem) =>
		item.value === undefined ||
		item.value === null ||
		(typeof item.value === 'string' && item.value.trim() === '');
</script>

<!-- Typed by the base item: a top-level snippet is hoisted outside the generic's scope. -->
{#snippet plain(item: KeyValueItem)}
	{#if item.href}
		<a
			href={item.href}
			class={[
				'key-value-display__link relative rounded-sm font-medium underline decoration-transparent underline-offset-4 transition-[text-decoration-color] duration-150 ease-(--_ease) hover:decoration-(--_link-line) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--_accent) motion-reduce:transition-none',
				item.mono && 'font-mono text-[0.8125rem]'
			]}
			>{item.value}&#8288;<svg
				class="ms-1 inline-block size-3.5 align-[-0.125em] text-(--_muted) rtl:-scale-x-100"
				viewBox="0 0 16 16"
				fill="none"
				stroke="currentColor"
				stroke-width="1.75"
				stroke-linecap="round"
				stroke-linejoin="round"
				aria-hidden="true"><path d="m6 3.5 4.5 4.5L6 12.5" /></svg
			></a
		>
	{:else if item.mono}
		<span class="font-mono text-[0.8125rem]">{item.value}</span>
	{:else}
		{item.value}
	{/if}
{/snippet}

{#if items.length > 0}
	<div class={['key-value-display @container', LABEL_WIDTHS[labelWidth], className]}>
		<dl class={shape.list}>
			{#each items as item, index (index)}
				<div class={[shape.row, dividers ? ['py-3', shape.rule] : 'py-2']}>
					<dt class="text-sm leading-6 text-pretty break-words text-(--_muted)">
						{item.label}
					</dt>
					<dd
						class="min-w-0 text-sm leading-6 text-pretty wrap-anywhere text-(--_ink) tabular-nums"
					>
						{#if missing(item)}
							<span class="text-(--_muted)" aria-hidden="true">{emptyValue}</span>
							<span class="sr-only">{emptyLabel}</span>
						{:else if value}
							{@render value(item, plain)}
						{:else}
							{@render plain(item)}
						{/if}
					</dd>
				</div>
			{/each}
		</dl>
	</div>
{/if}

<style>
	/*
	 * Public tokens: set --key-value-display-* on the list or any ancestor to retone it. Labels
	 * take muted, values ink, rules hairline, and the focus ring on value links takes accent.
	 */
	.key-value-display {
		--_accent: var(--key-value-display-accent, #18181b);
		--_ink: var(--key-value-display-ink, #18181b);
		--_muted: var(--key-value-display-muted, #52525b);
		--_hairline: var(--key-value-display-hairline, rgb(0 0 0 / 0.08));

		/* Formulas: a link's hover underline is the ink at low strength, and the state curve. */
		--_link-line: color-mix(in srgb, var(--_ink) 30%, transparent);
		--_ease: cubic-bezier(0.2, 0, 0, 1);
	}

	/*
	 * A pseudo-element box: value links are one text line tall, so this centres a hit area at
	 * least 44 px each way on the link without moving the row or stretching the focus ring.
	 * Centred on both axes, so it needs no direction-specific offset.
	 */
	.key-value-display__link::before {
		content: '';
		position: absolute;
		top: 50%;
		left: 50%;
		width: max(100%, 44px);
		height: max(100%, 44px);
		translate: -50% -50%;
	}
</style>

Pass items as already-formatted label and value pairs and pick a layout. It renders a description list and nothing else: no heading, card or panel around it, no number or date formatting, no copy buttons and no editing. An empty items array renders nothing, so show your own empty sentence.

Suggested location
src/lib/components/key-value-display-01
Required props
items

Limitations

  • Values are shown as given. Format numbers, currencies and dates before passing them, or render them in the value snippet.
  • The inline label column has a fixed width (7, 10 or 14 rem). Labels longer than it wrap; two lists on one page line up only when they share a labelWidth.
  • Inline and grid respond to the width of the list's container, not the viewport. A list in a 400 px sidebar stays stacked on a wide screen.
  • The value snippet only renders present values. Missing values always render the empty marker, so pass a value (it can be the badge's label) for any row the snippet should draw.
  • Light mode only by default. On a dark surface set --key-value-display-ink, -muted, -hairline and -accent (see the customisation guide).
  • Not a table. For values compared across several records, use a table.

Example

Svelte
<script lang="ts">
	import KeyValueList from '$lib/components/key-value-display-01/KeyValueList.svelte';

	const order = [
		{ label: 'Plan', value: 'Team, billed annually' },
		{ label: 'Seats', value: '14 × $12 per seat per month' },
		{ label: 'Invoice', value: 'INV-2026-0418', href: '/billing/invoices/0418' },
		{ label: 'VAT number' },
		{ label: 'Total', value: '$2,016.00 USD' }
	];
</script>

<KeyValueList items={order} />

<KeyValueList
	layout="grid"
	items={[
		{ label: 'Width', value: '180 cm' },
		{ label: 'Depth', value: '90 cm' },
		{ label: 'Height', value: '75 cm' }
	]}
/>

The source imports its own files by relative path only. Examples import from $lib/...; SvelteKit 3 projects have no $lib alias by default, so import from your #lib subpath (with the .svelte extension) or a relative path instead.

Key-value display#

A description list of label and value pairs: order summaries, product specs, account details, booking confirmations. It renders dl > div > dt + dd and nothing around it, so put it in your own panel or under your own heading.

Layouts#

  • inline (default): a label column and a value column. Every row shares the list's column tracks through subgrid, so all values start on one edge and each label sits on its value's first baseline. When the list's container is narrower than 28rem the rows stack.
  • stacked: label above value, for narrow columns and mobile-first panels.
  • grid: pairs in one, two or three columns by container width. Pairs in one row share their label and value lines, so a wrapped label does not knock its neighbours' values out of line.

Missing values#

value that is missing, null or blank renders emptyValue (an em dash) hidden from screen readers, with emptyLabel ("Not provided") as visually hidden text. Translate emptyLabel with your labels.

Custom values#

Svelte
<KeyValueList {items}>
	{#snippet value(item, plain)}
		{#if item.status}
			<Badge tone="success" dot>{item.value}</Badge>
		{:else}
			{@render plain(item)}
		{/if}
	{/snippet}
</KeyValueList>

Items can carry extra fields (status above); the snippet receives them typed. The snippet only renders present values: a row without a value always shows the empty marker.

Empty list#

An empty items array renders nothing at all. Show your own sentence in its place.

Props and content inputs#

On this page
NameTypeRequiredDefaultDescription
itemsT[] (T extends { label: string; value?: string | number | null; href?: string; mono?: boolean })YesNoneRows in order. A missing, null or blank value renders the empty marker; href makes the value a link; mono sets it in the monospace face. Extra fields reach the value snippet.
layout'stacked' | 'inline' | 'grid'No'inline'inline: label column and value column, stacking under a 28rem container. stacked: label above value. grid: pairs in one to three columns.
dividersbooleanNotrueHairline between rows (inline, stacked) or above each pair (grid). Off, rows sit closer and space alone separates them.
labelWidth'narrow' | 'medium' | 'wide'No'medium'Label column in the inline layout: 7, 10 or 14 rem.
emptyValuestringNo'—'Visible mark for a missing value. Hidden from assistive technology.
emptyLabelstringNo'Not provided'Visually hidden text a screen reader hears for a missing value. Translate it with the labels.
valueSnippet<[item: T, plain: Snippet<[KeyValueItem]>]>NoNoneCustom renderer for present values (a badge, a copy button, a formatted number). Call {@render plain(item)} for rows it does not customise.
classstringNoNoneExtra classes on the root, for margins or width.

Customization#

On this page

Pick layout, dividers and labelWidth through props. Retone through four --key-value-display-* variables. Edit the LAYOUTS and LABEL_WIDTHS maps in the source to change spacing, breakpoints or column widths.

  • Colours: --key-value-display-ink is the value colour, -muted the label and empty-marker colour, -hairline the dividers, and -accent the focus ring on value links.
  • Dark or tinted surface: set ink to #fafafa, muted to #a1a1aa, hairline to rgb(255 255 255 / 0.1) and accent to #fafafa (or your light brand colour) on the list or an ancestor, so the focus ring stays visible. The link underline is mixed from ink and follows. Check contrast on your surface.
  • Label column: labelWidth picks 7, 10 or 14 rem. For another width, change LABEL_WIDTHS, or pass class="[--_label-width:12rem]!" (the ! makes it beat the preset).
  • Breakpoints: inline stacks and grid adds columns on the container's width (@md is 28rem, @3xl 48rem). Change those variants in LAYOUTS to move them.
  • Density: rows are py-3 with dividers and py-2 without. Change both in the markup to tighten or loosen.
  • Badges and formatting: pass a value snippet. It receives each item and the default renderer, so you can render a badge for one row and leave the rest plain.
  • Hashes and IDs: set mono: true on the item, or wrap the value in your own copy button through the value snippet.

Public CSS variables

VariableToken
--key-value-display-accentaccent
--key-value-display-inkink
--key-value-display-mutedmuted
--key-value-display-hairlinehairline

Accessibility#

On this page
  • Renders dl > div > dt + dd, which is valid HTML; the div wrappers carry layout only. No role is added, so screen readers announce a description list with its terms and definitions.
  • A missing value shows an em dash hidden from assistive technology and a visually hidden 'Not provided' in its place. Translate emptyLabel along with your labels.
  • Value links are ordinary anchors set in medium weight with a trailing chevron (hidden from assistive technology), an underline on hover, a 44 px hit area and a two-pixel accent focus ring. Give each link text that makes sense out of context, or that the label beside it explains.
  • Default label and value colours clear 4.5:1 on white and zinc-50. Re-check if you retone muted.
  • The list has no heading of its own. Put it under a heading, or in a region labelled by one, so it is findable.

Known limitations

  • Contrast is checked for the default colours on light surfaces; retoned or dark surfaces are the consumer's to check.

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 · 21 October 2026

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