Typography
text() renders a piece of text at one of the library’s sizes. The variant picks a sensible element — variant: 'h2' renders an actual <h2> — so headings land in the document outline without anyone having to think about it.
Variants
Heading 1
Heading 2
Heading 3
Heading 4
Heading 5
Heading 6
Lead — one step up from body text, for the sentence under a title.
Body — the default.
Small — captions that are still sentences.
Caption — the small print.Overlinestack({ gap: 'sm' },
text({ variant: 'h1' }, 'Heading 1'),
text({ variant: 'h2' }, 'Heading 2'),
text({ variant: 'h3' }, 'Heading 3'),
text({ variant: 'h4' }, 'Heading 4'),
text({ variant: 'h5' }, 'Heading 5'),
text({ variant: 'h6' }, 'Heading 6'),
text({ variant: 'lead' }, 'Lead — one step up from body text, for the sentence under a title.'),
text({ variant: 'body' }, 'Body — the default.'),
text({ variant: 'small' }, 'Small — captions that are still sentences.'),
text({ variant: 'caption' }, 'Caption — the small print.'),
text({ variant: 'overline' }, 'Overline'),
)Headings
heading() takes an outline level and sizes itself to match. size decouples the two: an <h1> that looks like an h3 is still an h1 to a screen reader.
A level-2 heading, sized to match
A level-2 heading, sized like an h5
stack({ gap: 'sm' },
heading({ level: 2 }, 'A level-2 heading, sized to match'),
heading({ level: 2, size: 'h5' }, 'A level-2 heading, sized like an h5'),
)Tone
Three weights of emphasis, from full contrast down to the quietest readable grey.
Default — the colour body copy is set in.
Muted — secondary text, still comfortably readable.
Subtle — labels and metadata.
stack({ gap: 'xs' },
text('Default — the colour body copy is set in.'),
text({ tone: 'muted' }, 'Muted — secondary text, still comfortably readable.'),
text({ tone: 'subtle' }, 'Subtle — labels and metadata.'),
)Alignment
Start
Center
End
stack({ gap: 'xs' },
text({ align: 'start' }, 'Start'),
text({ align: 'center' }, 'Center'),
text({ align: 'end' }, 'End'),
)Truncating and clamping
truncate cuts a single line with an ellipsis. lines clamps to a number of lines instead, which is what a card summary usually wants.
A single line that keeps going well past the width of its container and gets cut off with an ellipsis rather than wrapping.
Clamped to two lines. This paragraph runs on for a while so that there is something for the clamp to actually cut, and then it keeps going a little longer still, past the point where the third line would have started.
stack({ gap: 'md' },
card({ variant: 'flat' }, cardBody(
text({ truncate: true }, 'A single line that keeps going well past the width of its container and gets cut off with an ellipsis rather than wrapping.'),
)),
card({ variant: 'flat' }, cardBody(
text({ lines: 2, tone: 'muted' }, 'Clamped to two lines. This paragraph runs on for a while so that there is something for the clamp to actually cut, and then it keeps going a little longer still, past the point where the third line would have started.'),
)),
)Inline code and keys
Run sitelo build or press ⌘ K to search.
text(
'Run ', code('sitelo build'), ' or press ', kbd('⌘'), ' ', kbd('K'), ' to search.',
)Children are rendered as HTML — that is what makes nesting work everywhere in this library, and code() is no exception. So a sample containing tags needs the text prop, which escapes them:
<em>Hello</em> — text: shown as written
Hello — children: parsed as markup
stack({ gap: 'sm' },
text(code({ text: '<em>Hello</em>' }), ' — text: shown as written'),
text(code('<em>Hello</em>'), ' — children: parsed as markup'),
)Both are useful. text is for a code sample, where a tag should be read rather than built. Children are for output that has already been syntax-highlighted, where the markup is the point — a Prism or Shiki result goes straight in.
sitelo build --root docs
sitelo build
stack({ gap: 'sm' },
text(code({ text: 'sitelo build --root docs' })),
text(code('<span style="color: var(--su-primary-soft-fg)">sitelo</span> build')),
)Composing
Text takes children, not just a string — so links, code and emphasis nest inside it the same way they would in HTML.
Pages are functions that return HTML. See the writing pages guide.
text({ variant: 'lead' },
'Pages are functions that return ',
code('HTML'),
'. See the ',
link({ href: '/docs/pages' }, 'writing pages'),
' guide.',
)Changing the element
as overrides the element without changing the look — for a visual heading that must not appear in the outline, or a <span> inside a line of text.
Caption styling on a paragraph
stack({ gap: 'xs' },
text({ variant: 'h4', as: 'div' }, 'Looks like a heading, is a div'),
text({ variant: 'caption', as: 'p' }, 'Caption styling on a paragraph'),
)Visually hidden
visuallyHidden() keeps content in the accessibility tree but off the screen — the label a screen reader needs where sighted readers get it from context.
Build status: passing — last build succeeded 4 minutes ago
text(
'Build status: ',
chip({ color: 'success', dot: true }, 'passing'),
visuallyHidden(' — last build succeeded 4 minutes ago'),
)Props
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'h1'…'h6' | 'lead' | 'body' | 'small' | 'caption' | 'overline' | 'body' | Size, weight and default element. |
tone | 'default' | 'muted' | 'subtle' | 'default' | How much contrast the text carries. |
align | 'start' | 'center' | 'end' | 'start' | Text alignment. |
truncate | boolean | false | One line, cut with an ellipsis. |
lines | number | — | Clamp to this many lines. |
as | string | — | Override the element the variant would pick. |
heading() takes level (1–6) and an optional size; everything else is the same.