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
Clear ideas, beautifully arranged
Clear ideas, beautifully arranged
Clear ideas, beautifully arranged
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
Large lead text
Body text
Small supporting text
Default
Muted
Quiet
Accent
Critical
Regular
Medium
Strong
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.
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
Billing
Plan and payment
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
Set BENTO_API_ORIGIN to point the proxy at another API.
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;
}
}