# Typography

> Package `@umriss-ui/core`, version 0.40.0. Demo page: <https://romanhaendler.github.io/umriss-ui/core/typography/>. Every page in one line: [llms.txt](https://romanhaendler.github.io/umriss-ui/core/llms.txt); every page in full: [llms-full.txt](https://romanhaendler.github.io/umriss-ui/core/llms-full.txt).

Sets paragraphs, headings and links on the library's type scale, so a screen of your own reads like the components around it. Reach for it wherever you would otherwise style text by hand.

```ts
import { Text, Heading, Link } from "@umriss-ui/core";
```

Demo page: https://romanhaendler.github.io/umriss-ui/core/typography/

A heading's outline level and its size are independent: choose the level for the page's structure, the size for the eye.

`Text` renders a `p`; inside a line, render it `as` a `span`.

## Examples

### Text, heading and link

A heading, a paragraph and a link in it, each at its default: the base of every surface.

```tsx
import { Heading, Link, Stack, Text } from "@umriss-ui/core";

export default function TextHeadingLink() {
  return (
    <Stack gap={2}>
      <Heading level={3}>Checkout</Heading>
      <Text>
        Takes card and wallet payments for every booking. Owned by the Payments team;{" "}
        <Link href="#/typography">see the on-call rota</Link>.
      </Text>
    </Stack>
  );
}
```

### Sizes

Six steps from `xs` to `2xl`; `md` is body text, `sm` and `xs` carry dense tables, hints and timestamps.

```tsx
import { Stack, Text } from "@umriss-ui/core";

export default function Sizes() {
  return (
    <Stack gap={2}>
      <Text size="2xl">2xl · Q1 budget review</Text>
      <Text size="xl">xl · Marketing is 12 % over plan</Text>
      <Text size="lg">lg · Cost centre CC-1200</Text>
      <Text size="md">md · Actuals are booked until the end of February.</Text>
      <Text size="sm">sm · Forecast updated by Rafael Ortiz</Text>
      <Text size="xs">xs · Last booking 17/03/2026 10:30</Text>
    </Stack>
  );
}
```

### Tones and weights

Step text back with `tone` rather than a colour of your own; `weight` has three steps, `medium` for emphasis inside a line. A `Heading` is semibold unless its `weight` says otherwise - `medium` for a quieter one.

```tsx
import { Heading, Stack, Text } from "@umriss-ui/core";

export default function TonesAndWeights() {
  return (
    <Stack gap={2}>
      <Heading level={3} size="md">
        Morning dispatch
      </Heading>
      <Heading level={4} size="md" weight="medium">
        Tours today
      </Heading>
      <Text weight="semibold">Tour T-03 · North depot</Text>
      <Text>
        14 stops, <Text as="span" weight="medium">2 running late</Text>
      </Text>
      <Text size="sm" tone="secondary">
        Driver Martin Hale · van FP 214 K
      </Text>
      <Text size="xs" tone="muted">
        Position reported 3 minutes ago
      </Text>
    </Stack>
  );
}
```

### Numbers and identifiers

Set figures and codes `mono`: tabular digits keep amounts in a column aligned, and 0 and O stay apart.

```tsx
import { Grid, Text } from "@umriss-ui/core";

const ROWS = [
  { centre: "CC-1100 Sales", actual: "138,412.00" },
  { centre: "CC-1200 Marketing", actual: "76,190.50" },
  { centre: "CC-2100 Engineering", actual: "181,007.25" },
  { centre: "CC-4400 Facilities", actual: "9,880.00" },
];

export default function Numbers() {
  return (
    <Grid columns={2} gap={2} style={{ maxWidth: 360 }}>
      {ROWS.flatMap((row) => [
        <Text key={`${row.centre}-name`} size="sm">
          {row.centre}
        </Text>,
        <Text key={`${row.centre}-actual`} size="sm" mono style={{ textAlign: "right" }}>
          {row.actual}
        </Text>,
      ])}
    </Grid>
  );
}
```

### Tracking and leading

Use `tracking="caps"` for small letter-spaced labels and `leading="tight"` for key figures and labels that run over two lines.

```tsx
import { Stack, Text } from "@umriss-ui/core";

export default function TrackingAndLeading() {
  return (
    <Stack gap={3} style={{ maxWidth: 280 }}>
      <Text size="xs" tracking="caps" tone="muted">
        Sprint 14 · Web team
      </Text>
      <Text size="2xl" tracking="display" leading="tight" weight="semibold">
        38 of 52 points
      </Text>
      <Text size="sm" leading="tight" tone="secondary">
        Checkout flow redesign and member portal sign-up, carried over from sprint 13
      </Text>
    </Stack>
  );
}
```

### Choose the level apart from the size

Pick `level` for the page's outline and `size` for the eye; a small card title can still be a second-level heading.

```tsx
import { Heading, Stack, Text } from "@umriss-ui/core";

export default function LevelAndSize() {
  return (
    <Stack gap={3}>
      <Heading level={2} size="sm" tone="secondary">
        Open incidents
      </Heading>
      <Heading level={3} size="xl">
        INC-1048 · Checkout slow, card payments time out
      </Heading>
      <Text size="sm" tone="secondary">
        A screen reader lists "Open incidents" as the section and the incident inside it, whatever
        their sizes.
      </Text>
    </Stack>
  );
}
```

### Link out of the application

Set `external` for a link that leaves the application: it opens a new tab and cuts the new page's access to this one.

```tsx
import { Link, Text } from "@umriss-ui/core";

export default function ExternalLink() {
  return (
    <Text size="sm">
      Card payments time out at the provider.{" "}
      <Link href="https://status.example.org" external>
        Provider status page
      </Link>{" "}
      · <Link href="#/typography">Incident INC-1048</Link>
    </Text>
  );
}
```

### Handle long values

Text wraps and headings balance their lines; where one line is a must, clip it with CSS of your own and keep the whole value in `title`.

```tsx
import { Heading, Stack, Text } from "@umriss-ui/core";

const CUSTOMER = "Holloway Garden Supplies & Landscaping Wholesale, Unit 14, Riverside Trading Estate";
const REFERENCE = "SH-1042/FP214K/2026-03-17T10:30:00Z/attempt-2";

export default function LongValues() {
  return (
    <Stack gap={3} style={{ maxWidth: 320 }}>
      <Heading level={4}>{CUSTOMER}</Heading>
      <Text size="sm" mono style={{ overflowWrap: "anywhere" }}>
        {REFERENCE}
      </Text>
      <Text
        size="sm"
        tone="secondary"
        title={CUSTOMER}
        style={{ whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}
      >
        {CUSTOMER}
      </Text>
    </Stack>
  );
}
```

### Set an incident summary

The pieces together in a detail panel: a caps label, a heading, mono identifiers and times, body text, a muted footnote and links.

```tsx
import { Badge, Card, CardBody, Heading, Link, Stack, Text } from "@umriss-ui/core";

/* Data from the operations world, written out here so the example runs on its own. */
const at = (day: number, hours: number, minutes = 0) => new Date(2026, 2, day, hours, minutes).getTime();

interface Service {
  id: string;
  name: string;
  team: string;
  /** 1 is customer-facing and pages at night; 3 waits for the morning. */
  tier: 1 | 2 | 3;
  /** The latency objective: the 95th percentile stays below this, in ms. */
  latencySlo: number;
  /** The availability promised for a month, in per cent. */
  availabilityTarget: number;
}

const SERVICES: readonly Service[] = [
  { id: "checkout", name: "Checkout", team: "Payments", tier: 1, latencySlo: 300, availabilityTarget: 99.95 },
  { id: "billing", name: "Billing", team: "Payments", tier: 1, latencySlo: 400, availabilityTarget: 99.9 },
  { id: "sign-in", name: "Sign-in", team: "Identity", tier: 1, latencySlo: 200, availabilityTarget: 99.95 },
  { id: "search", name: "Search", team: "Discovery", tier: 1, latencySlo: 250, availabilityTarget: 99.9 },
  { id: "images", name: "Image service", team: "Discovery", tier: 2, latencySlo: 500, availabilityTarget: 99.5 },
  { id: "notifications", name: "Notifications", team: "Messaging", tier: 2, latencySlo: 800, availabilityTarget: 99.5 },
  { id: "webhooks", name: "Webhooks", team: "Integrations", tier: 2, latencySlo: 1000, availabilityTarget: 99.5 },
  { id: "reports", name: "Reporting", team: "Insights", tier: 3, latencySlo: 2000, availabilityTarget: 99 },
];

interface Engineer {
  id: string;
  name: string;
  team: string;
}

const ENGINEERS: readonly Engineer[] = [
  { id: "priya", name: "Priya Raman", team: "Payments" },
  { id: "jonas", name: "Jonas Keller", team: "Payments" },
  { id: "ada", name: "Ada Mwangi", team: "Identity" },
  { id: "tomasz", name: "Tomasz Nowak", team: "Discovery" },
  { id: "leila", name: "Leila Haddad", team: "Discovery" },
  { id: "sam", name: "Sam Okafor", team: "Messaging" },
  { id: "ines", name: "Ines Duarte", team: "Integrations" },
  { id: "felix", name: "Felix Brandt", team: "Insights" },
];

interface Incident {
  id: string;
  title: string;
  service: string;
  severity: "SEV1" | "SEV2" | "SEV3";
  opened: number;
  acknowledged?: number;
  resolved?: number;
  /** An engineer's id. */
  assignee: string;
}

const INCIDENTS: readonly Incident[] = [
  { id: "INC-1048", title: "Checkout slow, card payments time out", service: "checkout", severity: "SEV1", opened: at(17, 9, 42), acknowledged: at(17, 9, 46), assignee: "jonas" },
  { id: "INC-1047", title: "Webhook deliveries delayed", service: "webhooks", severity: "SEV3", opened: at(17, 7, 15), acknowledged: at(17, 8, 2), assignee: "ines" },
  { id: "INC-1046", title: "Thumbnails missing for new uploads", service: "images", severity: "SEV2", opened: at(16, 22, 5), acknowledged: at(16, 22, 11), resolved: at(17, 0, 40), assignee: "tomasz" },
  { id: "INC-1045", title: "Sign-in codes arrive late", service: "sign-in", severity: "SEV2", opened: at(16, 14, 20), acknowledged: at(16, 14, 24), resolved: at(16, 15, 5), assignee: "ada" },
  { id: "INC-1044", title: "Monthly report export fails", service: "reports", severity: "SEV3", opened: at(15, 10, 0), acknowledged: at(16, 9, 12), resolved: at(16, 11, 30), assignee: "felix" },
  { id: "INC-1043", title: "Search returns no results for some regions", service: "search", severity: "SEV1", opened: at(14, 18, 30), acknowledged: at(14, 18, 33), resolved: at(14, 19, 55), assignee: "leila" },
  { id: "INC-1042", title: "Duplicate reminder e-mails", service: "notifications", severity: "SEV3", opened: at(13, 8, 45), acknowledged: at(13, 9, 30), resolved: at(13, 13, 0), assignee: "sam" },
  { id: "INC-1041", title: "Invoices generated twice", service: "billing", severity: "SEV2", opened: at(12, 16, 10), acknowledged: at(12, 16, 18), resolved: at(12, 18, 40), assignee: "priya" },
];

const incident = INCIDENTS[0]!;
const service = SERVICES.find((s) => s.id === incident.service)!;
const assignee = ENGINEERS.find((e) => e.id === incident.assignee)!;
const time = (t: number) =>
  new Date(t).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });

export default function IncidentSummary() {
  return (
    <Card>
      <CardBody>
        <Stack gap={3}>
          <Stack direction="row" gap={2} align="center">
            <Text size="xs" tracking="caps" tone="muted">
              {service.team} · {service.name}
            </Text>
            <Badge tone="danger">{incident.severity}</Badge>
          </Stack>
          <Heading level={3} size="lg">
            {incident.title}
          </Heading>
          <Text size="sm">
            Opened <Text as="span" mono>{time(incident.opened)}</Text>, acknowledged{" "}
            <Text as="span" mono>{time(incident.acknowledged!)}</Text> by {assignee.name}. The p95
            latency is above its {service.latencySlo} ms objective since 09:40.
          </Text>
          <Text size="sm">
            <Link href="#/typography">Open the latency chart</Link> ·{" "}
            <Link href="https://status.example.org" external>
              Payment provider status
            </Link>
          </Text>
          <Text size="xs" tone="muted">
            <Text as="span" size="xs" mono>
              {incident.id}
            </Text>{" "}
            · still open
          </Text>
        </Stack>
      </CardBody>
    </Card>
  );
}
```

## When to use something else

- Text only a screen reader should hear → VisuallyHidden
- A figure read against its limits, with unit and verdict → Stat
- A short status word in colour → Badge

## Keyboard

| Key | Action |
|---|---|
| `Tab` | Reaches each `Link`; `Text` and `Heading` are not stops. |
| `Enter` | Follows the link; an `external` one opens in a new tab. |

## API

### `TextProps`

| Prop | Type | Default | Description |
|---|---|---|---|
| `as` | `ElementType` | `"p"` | Element to render as.<br>Shown in: [Tones and weights](https://romanhaendler.github.io/umriss-ui/core/typography/#tones-and-weights), [Set an incident summary](https://romanhaendler.github.io/umriss-ui/core/typography/#incident-summary), [Use the language in your own components (Language)](https://romanhaendler.github.io/umriss-ui/core/language/#own-components) and 17 more |
| `size` | [`TextSize`](https://romanhaendler.github.io/umriss-ui/core/typography/#type-TextSize)<br>`"xs" \| "sm" \| "md" \| "lg" \| "xl" \| "2xl"` | `"md"` | Type size from the token set. It is independent of the element: a `p` may be small without becoming a different element.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/typography/#configurator-typography), [Sizes](https://romanhaendler.github.io/umriss-ui/core/typography/#sizes), [Tones and weights](https://romanhaendler.github.io/umriss-ui/core/typography/#tones-and-weights) and 129 more |
| `weight` | [`TextWeight`](https://romanhaendler.github.io/umriss-ui/core/typography/#type-TextWeight)<br>`"regular" \| "medium" \| "semibold"` | `"regular"` | Font weight from the token set.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/typography/#configurator-typography), [Tones and weights](https://romanhaendler.github.io/umriss-ui/core/typography/#tones-and-weights), [Tracking and leading](https://romanhaendler.github.io/umriss-ui/core/typography/#tracking-and-leading) and 15 more |
| `tone` | [`TextTone`](https://romanhaendler.github.io/umriss-ui/core/typography/#type-TextTone)<br>`"default" \| "secondary" \| "muted"` | `"default"` | How loud the text is. `secondary` and `muted` step back without falling below the contrast threshold.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/typography/#configurator-typography), [Tones and weights](https://romanhaendler.github.io/umriss-ui/core/typography/#tones-and-weights), [Tracking and leading](https://romanhaendler.github.io/umriss-ui/core/typography/#tracking-and-leading) and 94 more |
| `mono` | `boolean` | `false` | Geist Mono with tabular figures – for numbers and identifiers.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/typography/#configurator-typography), [Numbers and identifiers](https://romanhaendler.github.io/umriss-ui/core/typography/#numbers), [Handle long values](https://romanhaendler.github.io/umriss-ui/core/typography/#long-values) and 31 more |
| `tracking` | [`TextTracking`](https://romanhaendler.github.io/umriss-ui/core/typography/#type-TextTracking)<br>`"normal" \| "tight" \| "display" \| "caps"` | `"normal"` | Letter spacing from the token set. `caps` belongs with letterspaced capitals.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/typography/#configurator-typography), [Tracking and leading](https://romanhaendler.github.io/umriss-ui/core/typography/#tracking-and-leading), [Set an incident summary](https://romanhaendler.github.io/umriss-ui/core/typography/#incident-summary) |
| `leading` | [`TextLeading`](https://romanhaendler.github.io/umriss-ui/core/typography/#type-TextLeading)<br>`"normal" \| "tight"` | `"normal"` | Line spacing. `tight` for multi-line headings and key figures, where the normal spacing lets the lines fall apart.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/typography/#configurator-typography), [Tracking and leading](https://romanhaendler.github.io/umriss-ui/core/typography/#tracking-and-leading) |
| `ref` | `Ref<HTMLElement>` | — | Receives the component's root element. From `RefProp`.<br>Shown in: [A row that holds still (Sizes)](https://romanhaendler.github.io/umriss-ui/core/sizes/#a-row-that-holds-still), [An anchored surface (Popover)](https://romanhaendler.github.io/umriss-ui/core/popover/#anchored-surface), [Take the width of the trigger (Popover)](https://romanhaendler.github.io/umriss-ui/core/popover/#width-from-anchor) and 2 more |

Also takes every attribute of the rendered element.

### `HeadingProps`

| Prop | Type | Default | Description |
|---|---|---|---|
| `level` | `1 \| 2 \| 3 \| 4 \| 5 \| 6` | `2` | The page's outline level (h1–h6).<br>Shown in: [Text, heading and link](https://romanhaendler.github.io/umriss-ui/core/typography/#text-heading-link), [Tones and weights](https://romanhaendler.github.io/umriss-ui/core/typography/#tones-and-weights), [Choose the level apart from the size](https://romanhaendler.github.io/umriss-ui/core/typography/#level-and-size) and 6 more |
| `size` | [`TextSize`](https://romanhaendler.github.io/umriss-ui/core/typography/#type-TextSize)<br>`"xs" \| "sm" \| "md" \| "lg" \| "xl" \| "2xl"` | the size matching the level | Optical size; independent of the level.<br>Shown in: [Tones and weights](https://romanhaendler.github.io/umriss-ui/core/typography/#tones-and-weights), [Choose the level apart from the size](https://romanhaendler.github.io/umriss-ui/core/typography/#level-and-size), [Set an incident summary](https://romanhaendler.github.io/umriss-ui/core/typography/#incident-summary) and 4 more |
| `weight` | [`TextWeight`](https://romanhaendler.github.io/umriss-ui/core/typography/#type-TextWeight)<br>`"regular" \| "medium" \| "semibold"` | `"semibold"` | Font weight from the token set.<br>Shown in: [Tones and weights](https://romanhaendler.github.io/umriss-ui/core/typography/#tones-and-weights) |
| `tone` | [`TextTone`](https://romanhaendler.github.io/umriss-ui/core/typography/#type-TextTone)<br>`"default" \| "secondary" \| "muted"` | `"default"` | How loud the heading is.<br>Shown in: [Choose the level apart from the size](https://romanhaendler.github.io/umriss-ui/core/typography/#level-and-size) |
| `ref` | `Ref<HTMLHeadingElement>` | — | Receives the component's root element. From `RefProp`.<br>Shown in: [A row that holds still (Sizes)](https://romanhaendler.github.io/umriss-ui/core/sizes/#a-row-that-holds-still), [An anchored surface (Popover)](https://romanhaendler.github.io/umriss-ui/core/popover/#anchored-surface), [Take the width of the trigger (Popover)](https://romanhaendler.github.io/umriss-ui/core/popover/#width-from-anchor) and 2 more |

Also takes every attribute of the heading.

### `LinkProps`

| Prop | Type | Default | Description |
|---|---|---|---|
| `size` | [`TextSize`](https://romanhaendler.github.io/umriss-ui/core/typography/#type-TextSize)<br>`"xs" \| "sm" \| "md" \| "lg" \| "xl" \| "2xl"` | `"md"` | Size from the token set – a link has to match its body text.<br>Shown in: [Watch a kiln line over a shift (Scenarios)](https://romanhaendler.github.io/umriss-ui/core/#watch-a-kiln-line) |
| `external` | `boolean` | `false` | Opens in a new tab and sets the necessary protective attributes.<br>Shown in: [Link out of the application](https://romanhaendler.github.io/umriss-ui/core/typography/#external-link), [Set an incident summary](https://romanhaendler.github.io/umriss-ui/core/typography/#incident-summary) |
| `ref` | `Ref<HTMLAnchorElement>` | — | Receives the component's root element. From `RefProp`.<br>Shown in: [A row that holds still (Sizes)](https://romanhaendler.github.io/umriss-ui/core/sizes/#a-row-that-holds-still), [An anchored surface (Popover)](https://romanhaendler.github.io/umriss-ui/core/popover/#anchored-surface), [Take the width of the trigger (Popover)](https://romanhaendler.github.io/umriss-ui/core/popover/#width-from-anchor) and 2 more |

Also takes every attribute of `<a>`.

### Types on this page

#### `TextSize`

A type size of the token set.

```ts
type TextSize = "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
```

#### `TextWeight`

A font weight of the token set.

```ts
type TextWeight = "regular" | "medium" | "semibold";
```

#### `TextTone`

How loud a text is; `secondary` and `muted` step back without falling below
the contrast threshold.

```ts
type TextTone = "default" | "secondary" | "muted";
```

#### `TextTracking`

The letter spacing of the token set.

```ts
type TextTracking = "normal" | "tight" | "display" | "caps";
```

#### `TextLeading`

The line height of the token set.

```ts
type TextLeading = "normal" | "tight";
```

## Known limits

- No truncation and no line length: both depend on the column, which only the application knows.
- No prose styles for lists, quotes or rich text ([ADR-0032](https://romanhaendler.github.io/umriss-ui/what-umriss-ui-is-not/)).
- `Link` renders a plain `a` and knows no router; pass it the address your router resolves.

What umriss deliberately does not build, and why: [ADR-0032](https://romanhaendler.github.io/umriss-ui/what-umriss-ui-is-not/).
