Typography

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.

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

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

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

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.

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.

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.

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.

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.

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

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

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.

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.

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

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

NameTypeDefaultDescription
asElementType"p"Element to render as.

Shown in: Tones and weights, Set an incident summary, Use the language in your own components (Language) and 17 more

sizeTextSize
| "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.

Shown in: Configurator, Sizes, Tones and weights and 129 more

weightTextWeight
| "regular"
| "medium"
| "semibold"
"regular"Font weight from the token set.

Shown in: Configurator, Tones and weights, Tracking and leading and 15 more

toneTextTone
| "default"
| "secondary"
| "muted"
"default"How loud the text is. secondary and muted step back without falling below the contrast threshold.

Shown in: Configurator, Tones and weights, Tracking and leading and 94 more

monobooleanfalseGeist Mono with tabular figures – for numbers and identifiers.

Shown in: Configurator, Numbers and identifiers, Handle long values and 31 more

trackingTextTracking
| "normal"
| "tight"
| "display"
| "caps"
"normal"Letter spacing from the token set. caps belongs with letterspaced capitals.

Shown in: Configurator, Tracking and leading, Set an incident summary

leadingTextLeading
"normal" | "tight"
"normal"Line spacing. tight for multi-line headings and key figures, where the normal spacing lets the lines fall apart.

Shown in: Configurator, Tracking and leading

refRef<HTMLElement>—Receives the component's root element. from RefProp

Shown in: A row that holds still (Sizes), An anchored surface (Popover), Take the width of the trigger (Popover) and 2 more

Also takes every attribute of the rendered element.

HeadingProps

NameTypeDefaultDescription
level| 1
| 2
| 3
| 4
| 5
| 6
2The page's outline level (h1–h6).

Shown in: Text, heading and link, Tones and weights, Choose the level apart from the size and 6 more

sizeTextSize
| "xs"
| "sm"
| "md"
| "lg"
| "xl"
| "2xl"
the size matching the levelOptical size; independent of the level.

Shown in: Tones and weights, Choose the level apart from the size, Set an incident summary and 4 more

weightTextWeight
| "regular"
| "medium"
| "semibold"
"semibold"Font weight from the token set.

Shown in: Tones and weights

toneTextTone
| "default"
| "secondary"
| "muted"
"default"How loud the heading is.

Shown in: Choose the level apart from the size

refRef<HTMLHeadingElement>—Receives the component's root element. from RefProp

Shown in: A row that holds still (Sizes), An anchored surface (Popover), Take the width of the trigger (Popover) and 2 more

Also takes every attribute of the heading.

LinkProps

NameTypeDefaultDescription
sizeTextSize
| "xs"
| "sm"
| "md"
| "lg"
| "xl"
| "2xl"
"md"Size from the token set – a link has to match its body text.

Shown in: Watch a kiln line over a shift (Scenarios)

externalbooleanfalseOpens in a new tab and sets the necessary protective attributes.

Shown in: Link out of the application, Set an incident summary

refRef<HTMLAnchorElement>—Receives the component's root element. from RefProp

Shown in: A row that holds still (Sizes), An anchored surface (Popover), Take the width of the trigger (Popover) and 2 more

Also takes every attribute of <a>.

Types on this page

TextSize

A type size of the token set.

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

TextWeight

A font weight of the token set.

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

TextTone

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

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

TextTracking

The letter spacing of the token set.

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

TextLeading

The line height of the token set.

type TextLeading = "normal" | "tight";

Known limits

What umriss deliberately does not build, and why: ADR-0032.

Every page of @umriss-ui/core

Getting started

Customising

Layout

Typography

Actions

Forms

Choosing

Dates and times

Feedback

Overlays

Navigation

Data display

API index