Skip to examples
Bento / Kitchen sink
Bento / primitives

Display

Read-only presentation of records and values. Tones reinforce text; they never replace it.

Card

header · title · description · body · footer · elevation

Northstar

Workspace · 12 members

Active

The team's shared space for planning the autumn release.

Raised

For a card that sits above the page.

Seats used 18 / 25

A body alone, with no header or footer.

Footers line up. A footer sticks to the bottom, so cards of different heights in one row align their actions.

Source src/lib/components/display/card/doc.ts · src/lib/components/display/card/Card.svelte · src/lib/components/display/card/CardHeader.svelte · src/lib/components/display/card/CardTitle.svelte · src/lib/components/display/card/card.variants.ts · src/lib/components/display/card/card.module.css

src/lib/components/display/card/doc.ts

/**
 * Card — a framed unit of content.
 *
 * ┌───────────────────────────────────────────────────────────────────────────┐
 * │ § CONTRACT — the oracle. Names no library, contains no code.              │
 * └───────────────────────────────────────────────────────────────────────────┘
 *
 * # Shape
 *
 *     Card              as? ("article" | "section" | "div", default article),
 *                       elevation? ("flat" | "raised", default flat)
 *     CardHeader        children (title, description), actions?
 *     CardTitle         a Heading; level defaults to 3, size to "sm"
 *     CardDescription   a paragraph under the title
 *     CardBody          the content
 *     CardFooter        actions or metadata, end-aligned, above a rule
 *
 * # Behaviour
 *
 * R1  `flat` sits on `--surface-panel` with a `--line` border and `--radius-3`;
 *     `raised` sits on `--surface-raised` with `--shadow`.
 * R2  Every part pads with `--space-7`; a body following a header drops its
 *     top padding so the two read as one block.
 * R3  The footer sticks to the bottom, so cards of different content heights
 *     in one grid row align their footers.
 * R4  Header actions stay at the end of the title row and never wrap under
 *     the title's first line.
 * R5  A card is an article by default: something that stands on its own. Use
 *     a div when it only frames part of a page.
 *
 * # Deliberately absent (for now)
 *
 * A whole-card link and a media slot. A picture on a card is an AspectRatio
 * inside CardBody until a real screen asks for more.
 */
export {};

src/lib/components/display/card/Card.svelte

<script lang="ts">
	import type { Snippet } from 'svelte';
	import type { HTMLAttributes } from 'svelte/elements';
	import { cn } from '$lib/utils/cn';
	import { cardVariants, type CardVariants } from './card.variants';

	export type CardProps = Omit<HTMLAttributes<HTMLElement>, 'class'> &
		CardVariants & {
			/** An article when it stands on its own (a record, a post); a div when
			 *  it is only a frame around part of a page. */
			as?: 'article' | 'section' | 'div';
			class?: string;
			children?: Snippet;
		};

	let { as = 'article', elevation, class: className = '', children, ...rest }: CardProps = $props();
</script>

<svelte:element this={as} {...rest} class={cn(cardVariants({ elevation }), className)}>
	{@render children?.()}
</svelte:element>

src/lib/components/display/card/CardHeader.svelte

<script lang="ts">
	import type { Snippet } from 'svelte';
	import type { HTMLAttributes } from 'svelte/elements';
	import { cn } from '$lib/utils/cn';
	import styles from './card.module.css';

	let {
		actions,
		class: className = '',
		children,
		...rest
	}: Omit<HTMLAttributes<HTMLDivElement>, 'class'> & {
		/** Controls at the end of the header: a menu, a status, a link. */
		actions?: Snippet;
		class?: string;
		children?: Snippet;
	} = $props();
</script>

<div {...rest} class={cn(styles.header, className)}>
	<div class={styles.heading}>{@render children?.()}</div>
	{#if actions}<div class={styles.actions}>{@render actions()}</div>{/if}
</div>

src/lib/components/display/card/CardTitle.svelte

<script lang="ts">
	import Heading, { type HeadingProps } from '$lib/components/typography/heading/Heading.svelte';

	/* A heading, level 3 unless the page outline needs another. */
	let {
		level = 3,
		size = 'sm',
		...rest
	}: Omit<HeadingProps, 'level'> & { level?: HeadingProps['level'] } = $props();
</script>

<Heading {...rest} {level} {size} />

src/lib/components/display/card/card.variants.ts

import { cva, type VariantProps } from 'class-variance-authority';

import styles from './card.module.css';

export const cardVariants = cva(styles.root, {
	variants: {
		elevation: { flat: null, raised: styles.raised }
	},
	defaultVariants: { elevation: 'flat' }
});

export type CardVariants = VariantProps<typeof cardVariants>;

src/lib/components/display/card/card.module.css

@layer primitive {
	.root {
		display: flex;
		min-width: 0;
		flex-direction: column;
		border: 1px solid var(--line);
		border-radius: var(--radius-3);
		background: var(--surface-panel);
		color: var(--ink);
		overflow-wrap: anywhere;
	}
	.raised {
		background: var(--surface-raised);
		box-shadow: var(--shadow);
	}
	.header {
		display: flex;
		align-items: flex-start;
		gap: var(--space-5);
		padding: var(--space-7);
	}
	.heading {
		display: grid;
		min-width: 0;
		flex: 1;
		gap: var(--space-2);
	}
	.actions {
		display: flex;
		flex: none;
		align-items: center;
		gap: var(--space-3);
	}
	.description {
		margin: 0;
		color: var(--ink-2);
		font-size: var(--text-12);
		line-height: var(--leading-body);
	}
	.body {
		display: grid;
		min-width: 0;
		flex: 1;
		align-content: start;
		gap: var(--space-5);
		padding: var(--space-7);
	}
	/* The header already spaces the body from the card's top edge. */
	.header + .body {
		padding-top: 0;
	}
	.footer {
		display: flex;
		flex-wrap: wrap;
		align-items: center;
		justify-content: flex-end;
		gap: var(--space-4);
		margin-top: auto;
		padding: var(--space-5) var(--space-7);
		border-top: 1px solid var(--line);
	}
}

Badge

tone · dot

tone
neutral accent info warn crit
dot
Healthy Degraded Down
Source src/lib/components/display/badge/doc.ts · src/lib/components/display/badge/Badge.svelte · src/lib/components/display/badge/badge.variants.ts · src/lib/components/display/badge/badge.module.css

src/lib/components/display/badge/doc.ts

/**
 * Badge — a short status or category.
 *
 * ┌───────────────────────────────────────────────────────────────────────────┐
 * │ § CONTRACT — the oracle. Names no library, contains no code.              │
 * └───────────────────────────────────────────────────────────────────────────┘
 *
 * # Shape
 *
 *     tone?   "neutral" | "accent" | "info" | "warn" | "crit"   default neutral
 *     dot?    boolean
 *
 * # Behaviour
 *
 * R1  A pill: `--text-11`, strong, on the tone's tint with its line colour.
 * R2  The text is the meaning; tone and dot only reinforce it. `dot` adds a
 *     mark so the tone survives greyscale, hidden from assistive technology.
 * R3  One line, never wrapping.
 */
export {};

src/lib/components/display/badge/Badge.svelte

<script lang="ts">
	import type { Snippet } from 'svelte';
	import type { HTMLAttributes } from 'svelte/elements';
	import { cn } from '$lib/utils/cn';
	import styles from './badge.module.css';
	import { badgeVariants, type BadgeVariants } from './badge.variants';

	export type BadgeProps = Omit<HTMLAttributes<HTMLSpanElement>, 'class'> &
		BadgeVariants & {
			/** A dot before the text, so the tone does not rest on colour alone. */
			dot?: boolean;
			class?: string;
			children?: Snippet;
		};

	let { tone, dot = false, class: className = '', children, ...rest }: BadgeProps = $props();
</script>

<span {...rest} class={cn(badgeVariants({ tone }), className)}>
	{#if dot}<span class={styles.dot} aria-hidden="true"></span>{/if}
	{@render children?.()}
</span>

src/lib/components/display/badge/badge.variants.ts

import { cva, type VariantProps } from 'class-variance-authority';

import styles from './badge.module.css';

export const badgeVariants = cva(styles.root, {
	variants: {
		tone: {
			neutral: styles.neutral,
			accent: styles.accent,
			info: styles.info,
			warn: styles.warn,
			crit: styles.crit
		}
	},
	defaultVariants: { tone: 'neutral' }
});

export type BadgeVariants = VariantProps<typeof badgeVariants>;

src/lib/components/display/badge/badge.module.css

@layer primitive {
	.root {
		display: inline-flex;
		align-items: center;
		gap: var(--space-2);
		padding: 2px var(--space-4);
		border: 1px solid;
		border-radius: var(--radius-pill);
		font-size: var(--text-11);
		font-weight: var(--weight-strong);
		line-height: var(--leading-snug);
		white-space: nowrap;
	}
	.neutral {
		border-color: var(--line);
		background: var(--surface-hover);
		color: var(--ink-2);
	}
	.accent {
		border-color: var(--accent-line);
		background: var(--accent-tint);
		color: var(--accent);
	}
	.info {
		border-color: var(--info-line);
		background: var(--info-tint);
		color: var(--info);
	}
	.warn {
		border-color: var(--warn-line);
		background: var(--warn-tint);
		color: var(--warn);
	}
	.crit {
		border-color: var(--crit-line);
		background: var(--crit-tint);
		color: var(--crit);
	}
	/* A mark beside the colour, so the tone survives greyscale and colour
     blindness. The text is still the announcement. */
	.dot {
		width: 6px;
		height: 6px;
		flex: none;
		border-radius: var(--radius-pill);
		background: currentColor;
	}
}

Stat

label · value · hint — unmeasured is not zero

Active members 1,284 +8% this month
Median latency 42 ms p95 · all regions
Errors today 0
Churn Not measured Collected monthly

A missing value is a dash, never 0. “Errors today” measured zero; “Churn” was not measured, and is read as “Not measured”.

Source src/lib/components/display/stat/doc.ts · src/lib/components/display/stat/Stat.svelte · src/lib/components/display/stat/stat.module.css

src/lib/components/display/stat/doc.ts

/**
 * Stat — one headline number with its label.
 *
 * ┌───────────────────────────────────────────────────────────────────────────┐
 * │ § CONTRACT — the oracle. Names no library, contains no code.              │
 * └───────────────────────────────────────────────────────────────────────────┘
 *
 * # Shape
 *
 *     label   content, REQUIRED
 *     value?  content — undefined means "not measured"
 *     hint?   content — a comparison or a unit
 *
 * # Behaviour
 *
 * R1  The label sits above the value in the uppercase label style; the value
 *     is `--text-30` in the display face with tabular figures.
 * R2  An undefined value renders as a dash in `--ink-4` and is read as "Not
 *     measured". It never renders as 0: a zero is a measurement, and showing
 *     one for an unmeasured total asserts something nobody checked.
 */
export {};

src/lib/components/display/stat/Stat.svelte

<script lang="ts">
	import { VisuallyHidden } from '$lib/components/utility/visually-hidden';
	import { cn } from '$lib/utils/cn';
	import styles from './stat.module.css';

	export type StatProps = {
		label: string;
		/** `undefined` means nobody measured it, and renders as a dash rather than
		 *  0: a zero is a measurement, a dash is the absence of one. */
		value?: string | number;
		hint?: string;
		class?: string;
	};

	let { label, value, hint, class: className = '' }: StatProps = $props();
	const unmeasured = $derived(value === undefined);
</script>

<div class={cn(styles.root, className)}>
	<span class={styles.label}>{label}</span>
	<span class={cn(styles.value, unmeasured && styles.unmeasured)}>
		{#if unmeasured}<span aria-hidden="true">–</span><VisuallyHidden>Not measured</VisuallyHidden
			>{:else}{value}{/if}
	</span>
	{#if hint}<span class={styles.hint}>{hint}</span>{/if}
</div>

src/lib/components/display/stat/stat.module.css

@layer primitive {
	.root {
		display: grid;
		min-width: 0;
		gap: var(--space-2);
	}
	.label {
		color: var(--ink-3);
		font-size: var(--text-11);
		font-weight: var(--weight-strong);
		letter-spacing: var(--tracking-label);
		text-transform: uppercase;
	}
	.value {
		color: var(--ink);
		font-family: var(--font-display, var(--font-sans));
		font-size: var(--text-30);
		font-weight: var(--heading-weight, var(--weight-bold));
		font-variant-numeric: tabular-nums;
		line-height: var(--leading-tight);
		letter-spacing: var(--tracking-tight);
	}
	.unmeasured {
		color: var(--ink-4);
		font-weight: var(--weight-regular);
	}
	.hint {
		color: var(--ink-3);
		font-size: var(--text-12);
	}
}

Avatar

name · src · size

size
Sam Rivera Sam Rivera Sam Rivera
in a row
Alex Kim

Alex Kim

Owner

Source src/lib/components/display/avatar/doc.ts · src/lib/components/display/avatar/Avatar.svelte · src/lib/components/display/avatar/avatar.variants.ts · src/lib/components/display/avatar/avatar.module.css

src/lib/components/display/avatar/doc.ts

/**
 * Avatar — a person, as an image or initials.
 *
 * ┌───────────────────────────────────────────────────────────────────────────┐
 * │ § CONTRACT — the oracle. Names no library, contains no code.              │
 * └───────────────────────────────────────────────────────────────────────────┘
 *
 * # Shape
 *
 *     name    string, REQUIRED
 *     src?    image URL
 *     size?   "sm" (20px) | "md" (28px) | "lg" (40px)          default "md"
 *
 * # Behaviour
 *
 * R1  With `src`, the image fills a circle and `name` is its alternative
 *     text.
 * R2  Without it, up to two initials from `name` show on `--accent-tint`;
 *     the initials are hidden from assistive technology and `name` is read
 *     instead, so the avatar always identifies someone.
 * R3  `name` also shows as a tooltip on hover.
 */
export {};

src/lib/components/display/avatar/Avatar.svelte

<script lang="ts">
	import { VisuallyHidden } from '$lib/components/utility/visually-hidden';
	import { cn } from '$lib/utils/cn';
	import { initials } from './avatar';
	import styles from './avatar.module.css';
	import { avatarVariants, type AvatarVariants } from './avatar.variants';

	export type AvatarProps = AvatarVariants & {
		/** The person's name. Required even with an image: it is the image's
		 *  alternative text and the source of the initials. */
		name: string;
		src?: string;
		class?: string;
	};

	let { name, src, size, class: className = '' }: AvatarProps = $props();
</script>

<span class={cn(avatarVariants({ size }), className)} title={name}>
	{#if src}
		<img class={styles.image} {src} alt={name} />
	{:else}
		<span aria-hidden="true">{initials(name)}</span>
		<VisuallyHidden>{name}</VisuallyHidden>
	{/if}
</span>

src/lib/components/display/avatar/avatar.variants.ts

import { cva, type VariantProps } from 'class-variance-authority';

import styles from './avatar.module.css';

export const avatarVariants = cva(styles.root, {
	variants: { size: { sm: styles.sm, md: styles.md, lg: styles.lg } },
	defaultVariants: { size: 'md' }
});

export type AvatarVariants = VariantProps<typeof avatarVariants>;

src/lib/components/display/avatar/avatar.module.css

@layer primitive {
	.root {
		position: relative;
		display: inline-grid;
		flex: none;
		place-items: center;
		overflow: hidden;
		border: 1px solid var(--line);
		border-radius: var(--radius-pill);
		background: var(--accent-tint);
		color: var(--accent);
		font-weight: var(--weight-strong);
		user-select: none;
	}
	.sm {
		width: 20px;
		height: 20px;
		font-size: var(--text-9);
	}
	.md {
		width: 28px;
		height: 28px;
		font-size: var(--text-11);
	}
	.lg {
		width: 40px;
		height: 40px;
		font-size: var(--text-15);
	}
	.image {
		width: 100%;
		height: 100%;
		object-fit: cover;
	}
}

DescriptionList

terms and values

Workspace
Northstar
Plan
Team
Region
Europe (Ireland)
Created
12 March 2026
Source src/lib/components/display/description-list/doc.ts · src/lib/components/display/description-list/DescriptionList.svelte · src/lib/components/display/description-list/DescriptionItem.svelte · src/lib/components/display/description-list/description-list.module.css

src/lib/components/display/description-list/doc.ts

/**
 * DescriptionList — terms and their values.
 *
 * ┌───────────────────────────────────────────────────────────────────────────┐
 * │ § CONTRACT — the oracle. Names no library, contains no code.              │
 * └───────────────────────────────────────────────────────────────────────────┘
 *
 * # Shape
 *
 *     DescriptionList   the list
 *     DescriptionItem   term, REQUIRED; children are the value
 *
 * # Behaviour
 *
 * R1  Each pair is one row: the term in `--ink-3`, the value in `--ink`,
 *     separated from the next pair by a `--line` rule.
 * R2  Below 480px the term stacks above its value.
 * R3  Semantically a description list, so readers announce each term with
 *     its value.
 */
export {};

src/lib/components/display/description-list/DescriptionList.svelte

<script lang="ts">
	import type { Snippet } from 'svelte';
	import type { HTMLAttributes } from 'svelte/elements';
	import { cn } from '$lib/utils/cn';
	import styles from './description-list.module.css';

	let {
		class: className = '',
		children,
		...rest
	}: Omit<HTMLAttributes<HTMLDListElement>, 'class'> & {
		class?: string;
		children?: Snippet;
	} = $props();
</script>

<dl {...rest} class={cn(styles.root, className)}>{@render children?.()}</dl>

src/lib/components/display/description-list/DescriptionItem.svelte

<script lang="ts">
	import type { Snippet } from 'svelte';
	import type { HTMLAttributes } from 'svelte/elements';
	import { cn } from '$lib/utils/cn';
	import styles from './description-list.module.css';

	/* One term and its value. A div around each pair is valid inside a dl and
	   keeps the pair on one row. */
	let {
		term,
		class: className = '',
		children,
		...rest
	}: Omit<HTMLAttributes<HTMLDivElement>, 'class'> & {
		term: string;
		class?: string;
		children?: Snippet;
	} = $props();
</script>

<div {...rest} class={cn(styles.item, className)}>
	<dt class={styles.term}>{term}</dt>
	<dd class={styles.value}>{@render children?.()}</dd>
</div>

src/lib/components/display/description-list/description-list.module.css

@layer primitive {
	.root {
		margin: 0;
	}
	.item {
		display: grid;
		grid-template-columns: minmax(8rem, 1fr) minmax(0, 2fr);
		gap: var(--space-7);
		padding-block: var(--space-5);
		border-bottom: 1px solid var(--line);
		font-size: var(--text-body, var(--text-13));
		line-height: var(--leading-body);
	}
	.item:last-child {
		border-bottom: 0;
	}
	.term {
		color: var(--ink-3);
	}
	.value {
		min-width: 0;
		margin: 0;
		color: var(--ink);
		overflow-wrap: anywhere;
	}
	@media (max-width: 480px) {
		.item {
			grid-template-columns: 1fr;
			gap: var(--space-2);
		}
	}
}

Empty

nothing here, said calmly

No members yet

Invite people by email. They join this workspace as members.

Source src/lib/components/display/empty/doc.ts · src/lib/components/display/empty/Empty.svelte · src/lib/components/display/empty/empty.module.css

src/lib/components/display/empty/doc.ts

/**
 * Empty — a successful answer of "nothing here".
 *
 * ┌───────────────────────────────────────────────────────────────────────────┐
 * │ § CONTRACT — the oracle. Names no library, contains no code.              │
 * └───────────────────────────────────────────────────────────────────────────┘
 *
 * # Shape
 *
 *     title      content, REQUIRED — what is absent, said plainly
 *     children?  one or two sentences on why, or what fills it
 *     action?    the thing to do about it
 *
 * # Behaviour
 *
 * R1  Centred, calm, in ink and muted tones. Never styled as an error: an
 *     empty list is a working system, and red teaches people it is broken.
 * R2  The body text is capped at 44 characters per line.
 */
export {};

src/lib/components/display/empty/Empty.svelte

<script lang="ts">
	import type { Snippet } from 'svelte';
	import { cn } from '$lib/utils/cn';
	import styles from './empty.module.css';

	/* A successful answer of "nothing here". Styled as calm content, never as
	   an error: an empty list is a working system. */
	let {
		title,
		action,
		class: className = '',
		children
	}: {
		/** What is absent, said plainly: "No members yet", not "No data". */
		title: string;
		/** The thing to do about it, when there is one. */
		action?: Snippet;
		class?: string;
		children?: Snippet;
	} = $props();
</script>

<div class={cn(styles.root, className)}>
	<p class={styles.title}>{title}</p>
	{#if children}<p class={styles.body}>{@render children()}</p>{/if}
	{#if action}<div class={styles.action}>{@render action()}</div>{/if}
</div>

src/lib/components/display/empty/empty.module.css

@layer primitive {
	.root {
		display: grid;
		justify-items: center;
		gap: var(--space-4);
		padding: var(--space-9) var(--space-7);
		color: var(--ink-3);
		text-align: center;
	}
	.title {
		margin: 0;
		color: var(--ink);
		font-size: var(--text-15);
		font-weight: var(--weight-strong);
	}
	.body {
		max-width: 44ch;
		margin: 0;
		font-size: var(--text-body, var(--text-13));
		line-height: var(--leading-body);
	}
	.action {
		margin-top: var(--space-3);
	}
}