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.Overline
stack({ 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.

Looks like a heading, is a div

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

PropTypeDefaultDescription
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.
truncatebooleanfalseOne line, cut with an ellipsis.
linesnumberClamp to this many lines.
asstringOverride the element the variant would pick.

heading() takes level (1–6) and an optional size; everything else is the same.