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
Example
Display
Section
Card title
Subhead
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
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.
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
Example
Balance
$2,345.67Spent
$4,210.00Arrived
+$1,980.00import { 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
Example
Foundations
One voice, measured rather than chosen by eye.
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, 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 Default: level |
Text props
as TextElement | The element to render: Default: 'p' |
|---|---|
size TextSize |
Default: 'body' |
tone TextTone |
Default: 'default' |
Eyebrow props
as EyebrowElement | The element to render: 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.