Skip to examples
Bento / Kitchen sink
Bento / primitives

Typography

Text with a role, set in the active preset's type. Size is how text looks; level is where it sits in the outline, and the two are chosen separately.

Heading

level · size — level is required

xl

Clear ideas, beautifully arranged

lg

Clear ideas, beautifully arranged

md

Clear ideas, beautifully arranged

sm

Clear ideas, beautifully arranged

Level and size are independent. Every heading here is an h2; a sidebar title can be a small h2 and a hero a large h1. The weight comes from --heading-weight, which is medium in v4.

Source src/lib/components/typography/heading/doc.ts · src/lib/components/typography/heading/Heading.svelte · src/lib/components/typography/heading/heading.variants.ts · src/lib/components/typography/heading/heading.module.css

src/lib/components/typography/heading/doc.ts

/**
 * Heading — a title in the document outline.
 *
 * ┌───────────────────────────────────────────────────────────────────────────┐
 * │ § CONTRACT — the oracle. Names no library, contains no code.              │
 * └───────────────────────────────────────────────────────────────────────────┘
 *
 * # Shape
 *
 *     level    1 | 2 | 3 | 4 | 5 | 6, REQUIRED
 *     size?    "sm" | "md" | "lg" | "xl" | "display"  default "md"
 *     …every attribute of a heading, and a ref
 *
 * # Behaviour
 *
 * R1  Renders h1–h6 from `level`. Level is required and independent of size,
 *     because the outline is not a visual decision.
 * R2  Sizes: sm `--text-15`, md `--text-heading` (22px where unset), lg
 *     `--text-30`, xl fluid between `--text-30` and `--text-42`,
 *     display fluid between `--text-42` and `--text-54` (a page's one headline).
 * R3  Set in `--font-display` (the sans face where unset) at
 *     `--heading-weight`, with tight leading and tracking.
 * R4  Long words break rather than overflow; lines are balanced.
 *
 * ┌───────────────────────────────────────────────────────────────────────────┐
 * │ § MECHANICS — NOT the oracle.                                             │
 * └───────────────────────────────────────────────────────────────────────────┘
 *
 * `--heading-weight` exists so v4 can set its display face at medium weight
 * without a preset-specific rule in any stylesheet.
 */
export {};

src/lib/components/typography/heading/Heading.svelte

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

	export type HeadingProps = Omit<HTMLAttributes<HTMLHeadingElement>, 'class'> &
		HeadingVariants & {
			/** The document outline position. Required, and independent of `size`:
			 *  how big a heading looks is not where it sits in the outline. */
			level: 1 | 2 | 3 | 4 | 5 | 6;
			class?: string;
			children?: Snippet;
		};

	let { level, size, class: className = '', children, ...rest }: HeadingProps = $props();
</script>

<svelte:element this={`h${level}`} {...rest} class={cn(headingVariants({ size }), className)}>
	{@render children?.()}
</svelte:element>

src/lib/components/typography/heading/heading.variants.ts

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

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

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

export type HeadingVariants = VariantProps<typeof headingVariants>;

src/lib/components/typography/heading/heading.module.css

@layer primitive {
	.root {
		margin: 0;
		color: var(--ink);
		font-family: var(--font-display, var(--font-sans));
		font-weight: var(--heading-weight, var(--weight-bold));
		line-height: var(--leading-tight);
		letter-spacing: var(--tracking-tight);
		overflow-wrap: anywhere;
		text-wrap: balance;
	}
	.sm {
		font-size: var(--text-15);
	}
	.md {
		font-size: var(--text-heading, var(--text-22));
	}
	.lg {
		font-size: var(--text-30);
	}
	.xl {
		font-size: clamp(var(--text-30), 4vw, var(--text-42));
	}
	/* A page's one headline: a hero, a coming-soon page. */
	.display {
		font-size: clamp(var(--text-42), 6vw, var(--text-54));
		letter-spacing: calc(var(--tracking-tight) * 1.5);
	}
}

Text

size · tone · weight · measure · truncate

size

Large lead text

Body text

Small supporting text

tone

Default

Muted

Quiet

Accent

Critical

weight

Regular

Medium

Strong

measure

A useful interface makes the next step obvious and gives every detail just enough room to breathe. Capped at the preset's reading measure, this paragraph stays a comfortable number of characters per line however wide its container gets.

truncate

workspaces/northstar/projects/autumn-release/notes/final.md

Source src/lib/components/typography/text/doc.ts · src/lib/components/typography/text/Text.svelte · src/lib/components/typography/text/text.variants.ts · src/lib/components/typography/text/text.module.css

src/lib/components/typography/text/doc.ts

/**
 * Text — body copy.
 *
 * ┌───────────────────────────────────────────────────────────────────────────┐
 * │ § CONTRACT — the oracle. Names no library, contains no code.              │
 * └───────────────────────────────────────────────────────────────────────────┘
 *
 * # Shape
 *
 *     as?         "p" | "span" | "div"                            default "p"
 *     size?       "sm" | "md" | "lg"                              default "md"
 *     tone?       "default" | "muted" | "quiet" | "accent" | "crit"
 *     weight?     "regular" | "medium" | "strong"
 *     measure?    boolean — cap the line length at `--measure`
 *     truncate?   boolean — one line, ending in an ellipsis
 *
 * # Behaviour
 *
 * R1  md is `--text-body` (13px where unset), sm `--text-12`, lg `--text-15`,
 *     all at body leading.
 * R2  Tones map to `--ink`, `--ink-2`, `--ink-3`, `--accent`, and `--crit`.
 *     Tone is emphasis, never the only carrier of meaning.
 * R3  `truncate` hides overflow on one line; the full text stays in the DOM
 *     and is read in full by assistive technology.
 */
export {};

src/lib/components/typography/text/Text.svelte

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

	export type TextProps = Omit<HTMLAttributes<HTMLElement>, 'class'> &
		TextVariants & {
			/** A paragraph by default; a span for text inside a line. */
			as?: 'p' | 'span' | 'div';
			class?: string;
			children?: Snippet;
		};

	let {
		as = 'p',
		size,
		tone,
		weight,
		measure,
		truncate,
		class: className = '',
		children,
		...rest
	}: TextProps = $props();
</script>

<svelte:element
	this={as}
	{...rest}
	class={cn(textVariants({ size, tone, weight, measure, truncate }), className)}
>
	{@render children?.()}
</svelte:element>

src/lib/components/typography/text/text.variants.ts

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

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

export const textVariants = cva(styles.root, {
	variants: {
		size: { sm: styles.sm, md: styles.md, lg: styles.lg },
		tone: {
			default: styles.default,
			muted: styles.muted,
			quiet: styles.quiet,
			accent: styles.accent,
			crit: styles.crit
		},
		weight: {
			regular: styles.regular,
			medium: styles.medium,
			strong: styles.strong
		},
		measure: { true: styles.measure },
		truncate: { true: styles.truncate }
	},
	defaultVariants: { size: 'md', tone: 'default', weight: 'regular' }
});

export type TextVariants = VariantProps<typeof textVariants>;

src/lib/components/typography/text/text.module.css

@layer primitive {
	.root {
		margin: 0;
		line-height: var(--leading-body);
	}
	.sm {
		font-size: var(--text-12);
	}
	.md {
		font-size: var(--text-body, var(--text-13));
	}
	.lg {
		font-size: var(--text-15);
	}
	.default {
		color: var(--ink);
	}
	.muted {
		color: var(--ink-2);
	}
	.quiet {
		color: var(--ink-3);
	}
	.accent {
		color: var(--accent);
	}
	.crit {
		color: var(--crit);
	}
	.regular {
		font-weight: var(--weight-regular);
	}
	.medium {
		font-weight: var(--weight-medium);
	}
	.strong {
		font-weight: var(--weight-strong);
	}
	.measure {
		max-width: var(--measure);
	}
	.truncate {
		display: block;
		overflow: hidden;
		text-overflow: ellipsis;
		white-space: nowrap;
	}
}

SectionLabel

an eyebrow; not part of the outline

accent

Billing

Plan and payment

quiet

Last 30 days

Source src/lib/components/typography/section-label/doc.ts · src/lib/components/typography/section-label/SectionLabel.svelte · src/lib/components/typography/section-label/section-label.variants.ts · src/lib/components/typography/section-label/section-label.module.css

src/lib/components/typography/section-label/doc.ts

/**
 * SectionLabel — a small uppercase label above a heading or group.
 *
 * ┌───────────────────────────────────────────────────────────────────────────┐
 * │ § CONTRACT — the oracle. Names no library, contains no code.              │
 * └───────────────────────────────────────────────────────────────────────────┘
 *
 * # Shape
 *
 *     as?     "p" | "span"                default "p"
 *     tone?   "accent" | "quiet"          default "accent"
 *
 * # Behaviour
 *
 * R1  `--text-11`, bold, uppercase, `--tracking-label`.
 * R2  It is not a heading and does not enter the outline. Screen readers read
 *     it as plain text before the heading it sits above.
 */
export {};

src/lib/components/typography/section-label/SectionLabel.svelte

<script lang="ts">
	import type { Snippet } from 'svelte';
	import type { HTMLAttributes } from 'svelte/elements';
	import { cn } from '$lib/utils/cn';
	import { sectionLabelVariants, type SectionLabelVariants } from './section-label.variants';

	/* A small uppercase label above a heading or a group ("an eyebrow"). It is
	   not a heading: it does not enter the document outline. */
	export type SectionLabelProps = Omit<HTMLAttributes<HTMLElement>, 'class'> &
		SectionLabelVariants & {
			as?: 'p' | 'span';
			class?: string;
			children?: Snippet;
		};

	let { as = 'p', tone, class: className = '', children, ...rest }: SectionLabelProps = $props();
</script>

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

src/lib/components/typography/section-label/section-label.variants.ts

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

import styles from './section-label.module.css';

export const sectionLabelVariants = cva(styles.root, {
	variants: { tone: { accent: styles.accent, quiet: styles.quiet } },
	defaultVariants: { tone: 'accent' }
});

export type SectionLabelVariants = VariantProps<typeof sectionLabelVariants>;

src/lib/components/typography/section-label/section-label.module.css

@layer primitive {
	.root {
		margin: 0;
		font-size: var(--text-11);
		font-weight: var(--weight-bold);
		line-height: var(--leading-snug);
		letter-spacing: var(--tracking-label);
		text-transform: uppercase;
	}
	.accent {
		color: var(--accent);
	}
	.quiet {
		color: var(--ink-3);
	}
}

Code

inline, sized to its line

inline

Set BENTO_API_ORIGIN to point the proxy at another API.

in a heading

The bento_session cookie

Source src/lib/components/typography/code/doc.ts · src/lib/components/typography/code/Code.svelte · src/lib/components/typography/code/code.module.css

src/lib/components/typography/code/doc.ts

/**
 * Code — inline code.
 *
 * ┌───────────────────────────────────────────────────────────────────────────┐
 * │ § CONTRACT — the oracle. Names no library, contains no code.              │
 * └───────────────────────────────────────────────────────────────────────────┘
 *
 * # Behaviour
 *
 * R1  Monospace at 0.9em of the surrounding text, on `--surface-sunk` with a
 *     `--line` border, so it fits any line it sits in.
 * R2  Long identifiers break anywhere rather than overflow.
 * R3  Inline only. A block of code is a different component.
 */
export {};

src/lib/components/typography/code/Code.svelte

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

	/* Inline code: an identifier, a key, a command. Sized relative to the text
	   around it, so it fits any line it sits in. */
	let {
		class: className = '',
		children,
		...rest
	}: Omit<HTMLAttributes<HTMLElement>, 'class'> & { class?: string; children?: Snippet } = $props();
</script>

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

src/lib/components/typography/code/code.module.css

@layer primitive {
	.root {
		padding: 0.1em 0.35em;
		border: 1px solid var(--line);
		border-radius: var(--radius-1);
		background: var(--surface-sunk);
		color: var(--ink);
		font-family: var(--font-mono);
		font-size: 0.9em;
		overflow-wrap: anywhere;
	}
}

Kbd

keys and combinations

Press ⌘K to search, Esc to close, and Shift↑ to step by ten.

Source src/lib/components/typography/kbd/doc.ts · src/lib/components/typography/kbd/Kbd.svelte · src/lib/components/typography/kbd/kbd.module.css

src/lib/components/typography/kbd/doc.ts

/**
 * Kbd — keys to press.
 *
 * ┌───────────────────────────────────────────────────────────────────────────┐
 * │ § CONTRACT — the oracle. Names no library, contains no code.              │
 * └───────────────────────────────────────────────────────────────────────────┘
 *
 * # Shape
 *
 *     keys   one key, or a combination: ["⌘", "K"]
 *
 * # Behaviour
 *
 * R1  A combination is one kbd element holding a kbd per key, as HTML nests
 *     them; each key is read out.
 * R2  It scales with the text around it.
 */
export {};

src/lib/components/typography/kbd/Kbd.svelte

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

	/* Keys to press. A combination is one <kbd> of <kbd>s, as HTML nests
	   them, and is read out key by key. */
	let {
		keys,
		class: className = ''
	}: {
		/** One key, or a combination pressed together: ['⌘', 'K']. */
		keys: string | readonly string[];
		class?: string;
	} = $props();

	const list = $derived(typeof keys === 'string' ? [keys] : keys);
</script>

<kbd class={cn(styles.root, className)}
	>{#each list as key, index (`${index}-${key}`)}<kbd class={styles.key}>{key}</kbd>{/each}</kbd
>

src/lib/components/typography/kbd/kbd.module.css

@layer primitive {
	.root {
		display: inline-flex;
		align-items: center;
		gap: 2px;
		font-family: var(--font-sans);
		white-space: nowrap;
	}
	.key {
		display: inline-grid;
		min-width: 1.6em;
		height: 1.6em;
		place-items: center;
		padding: 0 0.35em;
		border: 1px solid var(--line-strong);
		border-bottom-width: 2px;
		border-radius: var(--radius-1);
		background: var(--surface-sunk);
		color: var(--ink-2);
		font: inherit;
		font-size: 0.8em;
		line-height: 1;
	}
}