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.
Sourcecomponents/typography/heading/doc.ts · components/typography/heading/heading.tsx · components/typography/heading/heading.variants.ts · components/typography/heading/heading.module.css
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 {};components/typography/heading/heading.tsx
import type { ComponentPropsWithRef } from "react";
import { cn } from "@/lib/utils/cn";
import { headingVariants, type HeadingVariants } from "./heading.variants";
export type HeadingProps = ComponentPropsWithRef<"h2"> &
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;
};
export function Heading({ level, size, className, ...props }: HeadingProps) {
const Tag = `h${level}` as const;
return (
<Tag {...props} className={cn(headingVariants({ size }), className)} />
);
}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>;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
Sourcecomponents/typography/text/doc.ts · components/typography/text/text.tsx · components/typography/text/text.variants.ts · components/typography/text/text.module.css
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 {};components/typography/text/text.tsx
import type { ComponentPropsWithRef } from "react";
import { cn } from "@/lib/utils/cn";
import { textVariants, type TextVariants } from "./text.variants";
export type TextProps = ComponentPropsWithRef<"p"> &
TextVariants & {
/** A paragraph by default; a span for text inside a line. */
as?: "p" | "span" | "div";
};
export function Text({
as: Tag = "p",
size,
tone,
weight,
measure,
truncate,
className,
...props
}: TextProps) {
return (
<Tag
{...props}
className={cn(
textVariants({ size, tone, weight, measure, truncate }),
className,
)}
/>
);
}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>;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
Sourcecomponents/typography/section-label/doc.ts · components/typography/section-label/section-label.tsx · components/typography/section-label/section-label.variants.ts · components/typography/section-label/section-label.module.css
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 {};components/typography/section-label/section-label.tsx
import type { ComponentPropsWithRef } from "react";
import { cn } from "@/lib/utils/cn";
import {
sectionLabelVariants,
type SectionLabelVariants,
} from "./section-label.variants";
export type SectionLabelProps = ComponentPropsWithRef<"p"> &
SectionLabelVariants & {
as?: "p" | "span";
};
/** 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 function SectionLabel({
as: Tag = "p",
tone,
className,
...props
}: SectionLabelProps) {
return (
<Tag {...props} className={cn(sectionLabelVariants({ tone }), className)} />
);
}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>;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
Sourcecomponents/typography/code/doc.ts · components/typography/code/code.tsx · components/typography/code/code.module.css
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 {};components/typography/code/code.tsx
import type { ComponentPropsWithRef } from "react";
import { cn } from "@/lib/utils/cn";
import styles from "./code.module.css";
export type CodeProps = ComponentPropsWithRef<"code">;
/** Inline code: an identifier, a key, a command. Sized relative to the text
* around it, so it fits any line it sits in. */
export function Code({ className, ...props }: CodeProps) {
return <code {...props} className={cn(styles.root, className)} />;
}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.
Sourcecomponents/typography/kbd/doc.ts · components/typography/kbd/kbd.tsx · components/typography/kbd/kbd.module.css
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 {};components/typography/kbd/kbd.tsx
import { cn } from "@/lib/utils/cn";
import styles from "./kbd.module.css";
export type KbdProps = {
/** One key, or a combination pressed together: ["⌘", "K"]. */
keys: string | readonly string[];
className?: string;
};
/** Keys to press. A combination is one <kbd> of <kbd>s, as HTML nests
* them, and is read out key by key. */
export function Kbd({ keys, className }: KbdProps) {
const list = typeof keys === "string" ? [keys] : keys;
return (
<kbd className={cn(styles.root, className)}>
{list.map((key, index) => (
<kbd key={`${index}-${key}`} className={styles.key}>
{key}
</kbd>
))}
</kbd>
);
}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;
}
}