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.

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

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

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

accent

Plan and payment

quiet
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

inline

Set BENTO_API_ORIGIN to point the proxy at another API.

in a heading

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;
  }
}