Heading, Text and Eyebrow set the type scale as plain elements that work in server components.

Heading

level picks the element and size picks the look. The size follows the level unless you set it, so a card title can be an h2 in the card-title step.

Example

Display

Section

Card title

Subhead

Follows the page theme and corners
import { Heading } from '@snapshot-ui/react';
import type { ReactElement } from 'react';

export default function HeadingSizes(): ReactElement {
  return (
    <div style={{ display: 'grid', gap: 16 }}>
      <Heading level={3} size={1}>
        Display
      </Heading>
      <Heading level={3} size={2}>
        Section
      </Heading>
      <Heading level={3} size={3}>
        Card title
      </Heading>
      <Heading level={3} size={4}>
        Subhead
      </Heading>
    </div>
  );
}

Headings scale down on small screens:

  • Display: 48px under 1024px, 40px under 768px.
  • Section: 28px under 768px.

Text

Body text in the body or the small step. The muted tone is slate, for secondary text.

Example

Changes apply to everyone in the workspace.

Used in invoices, emails and the receipt page.

Last saved 4 minutes ago.

Only you can see this.

Follows the page theme and corners
import { Text } from '@snapshot-ui/react';
import type { ReactElement } from 'react';

export default function TextTones(): ReactElement {
  return (
    <div style={{ display: 'grid', gap: 12, maxWidth: 420 }}>
      <Text>Changes apply to everyone in the workspace.</Text>
      <Text size="small">Used in invoices, emails and the receipt page.</Text>
      <Text tone="muted">Last saved 4 minutes ago.</Text>
      <Text size="small" tone="muted">
        Only you can see this.
      </Text>
    </div>
  );
}

Numerals

Use a numeral size for a figure that is the point of a tile. Both use tabular digits.

  • size="numeral": a hero amount, 56px, dropping to 44px under 768px so "$12,345.67" fits a 320px screen.
  • size="numeral-sm": a tile beside others, 32px.

A numeral never wraps, because a broken number reads wrong. Keep it to one figure. Two numeral-sm tiles cannot share a row at 320px, so let the row wrap (flex-wrap), as the example does. Put the sign and unit in the string, since nothing is formatted here. A numeral reads left to right in every direction, so "+$1,980.00" keeps its order on a right-to-left page.

Example

Balance

$2,345.67

Spent

$4,210.00

Arrived

+$1,980.00
Follows the page theme and corners
import { Eyebrow, Text } from '@snapshot-ui/react';
import type { ReactElement } from 'react';

export default function TextNumerals(): ReactElement {
  return (
    <div style={{ display: 'grid', gap: 32, justifyItems: 'start' }}>
      <div style={{ display: 'grid', gap: 8 }}>
        <Eyebrow>Balance</Eyebrow>
        {/* A 44px "$12,345.67" is 230px, which does not fit a 212px column, so this hero shows a shorter figure. */}
        <Text as="span" size="numeral">
          $2,345.67
        </Text>
      </div>
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: '24px 32px' }}>
        <div style={{ display: 'grid', gap: 8 }}>
          <Eyebrow>Spent</Eyebrow>
          <Text as="span" size="numeral-sm">
            $4,210.00
          </Text>
        </div>
        <div style={{ display: 'grid', gap: 8 }}>
          <Eyebrow>Arrived</Eyebrow>
          <Text as="span" size="numeral-sm" style={{ color: 'var(--color-gain)' }}>
            +$1,980.00
          </Text>
        </div>
      </div>
    </div>
  );
}

Eyebrow

The small capitalised label above a heading, with the accent dot. The dot hangs into the gutter, so the label lines up with the heading below. With no gutter, inset keeps the dot inside the label's box. An Eyebrow is an inline-level flex box, so it sits on a line like text, and two can share a line. Space it from the heading with a grid or flex gap, as the examples do, not with margins.

Example

Foundations

One voice, measured rather than chosen by eye.

Follows the page theme and corners
import { Eyebrow, Heading } from '@snapshot-ui/react';
import type { ReactElement } from 'react';

export default function EyebrowHeading(): ReactElement {
  return (
    <div style={{ display: 'grid', gap: 8, maxWidth: 420 }}>
      <Eyebrow>Foundations</Eyebrow>
      <Heading level={3} size={2}>
        One voice, measured rather than chosen by eye.
      </Heading>
    </div>
  );
}

Usage

None of the three needs a client boundary: they render plain elements and import no React Aria.

import { Eyebrow, Heading, Text } from '@snapshot-ui/react';

export function SectionIntro() {
  return (
    <header>
      <Eyebrow>Foundations</Eyebrow>
      <Heading level={2}>Everything a dashboard asks for.</Heading>
      <Text tone="muted">Type and color come from tokens.</Text>
    </header>
  );
}

Props

Each accepts every attribute of the element it renders, and a className added after its own.

Heading props

level
HeadingLevel

The element, h1 to h4. Choose it by the heading's place in the page outline, not by how it should look.

Default: 2

size
HeadingLevel

The look: 1 is the 64px display step, 2 the 36px section step, 3 the 24px card title and 4 the 14px bold subhead. Follows level unless set.

Default: level

Text props

as
TextElement

The element to render: p, span or div.

Default: 'p'

size
TextSize

body is 16px. small is 14px, for hints. numeral (56px, 44px under 768px) and numeral-sm (32px) are tabular figures that never wrap, for a tile's main amount. Put the sign and unit in the string.

Default: 'body'

tone
TextTone

default takes the color around it; muted is slate, for secondary text.

Default: 'default'

Eyebrow props

as
EyebrowElement

The element to render: p, span or div. An eyebrow is a label, never a heading.

Default: 'p'

inset
boolean

Keeps the dot inside the label's box. By default the dot hangs into the gutter beside the label, so use this where there is no gutter.

Default: false

Accessibility

A Heading is a real h1 to h4 element, so assistive technology reads the page outline whatever size it is drawn in. An Eyebrow is a label, never a heading, so it stays out of the outline. Muted text is slate, which clears 4.5:1 on every surface.

A numeral is read as the digits it shows, so the sign and unit belong in the text. A change line beside it says the direction in a word, never in color alone.

Design rules

What keeps the type in the system.

Pick the level from the page outline, then the size from the hierarchy.

Use one h1 per page and never skip a level on the way down.

Leave room beside an eyebrow: its dot hangs 14px into the gutter. With no gutter, set inset.

Never pick a level for its size. Set the size instead.

No capitals except the eyebrow.

Never taupe for text a reader has to read. Muted is slate.

esc

↑ ↓ to move↵ to open