Typography & portable text

Everything below is authored in the Studio's portable-text editor and rendered through this project's serializers. It doubles as a visual regression check: if heading spacing or the type scale drifts, it shows up here first.

The heading scale

Headings carry no vertical margin of their own — spacing is owned by the .Nuxion-flow wrapper the renderer emits, using adjacent-sibling rules. The gap above a heading is roughly three times the gap below it, so each heading reads as belonging to the text that follows rather than floating between two paragraphs.

A third-level heading

Because those margins are em-based, every level separates in proportion to its own size: an h2 breaks a page more strongly than an h3 without either being hand-tuned.

A fourth-level heading

The first block in any portable-text field never receives a top margin, which is what lets heroes and CTA banners render their title through this same system without being pushed down the page.

Fifth level
Sixth level

Note that on a blog post the whole scale steps down: 48px display headings suit a marketing section title, but sit far too large inside 18px body copy.

Decorators

Decorators are toggles applied to a text range — no configuration, no markDefs entry.

Bold, emphasis, underline, strikethrough, inline code, monospace, uppercase, light weight and extra bold. They compose, so text can be bold and monospaced at once.

Alignment

Alignment is also a decorator. This line is centred.

And this one is aligned right.

Annotations

Annotations attach data to a range, so each one stores an entry in the block's markDefs and the span references it by key.

Heading style

The heading annotation renders an inline heading inside a paragraph — a span, not a real h-tag. It picks up the heading type scale but not the vertical rhythm, because vertical margins don't apply to inline elements.

Font size and line height

Font size is set per range: this text is larger than its neighbours.

Line height is also an annotation, so it can only ever be inline. A line box takes the greater of the paragraph's own line height and any inline box on it, so values below the paragraph's setting have no visible effect — only the loosest option changes anything in body copy.

Colour and links

Highlight recolours text from the site colour palette, so it re-themes with the rest of the site.

Links come in four flavours: external (opens in new tab), internal (resolved from the referenced page's slug, so it survives a rename), email and telephone.

Gradient text derives its colours from the active theme when set to inherit, so it recolours with the rest of the page.

Width and alignment constrains a range to a maximum width and aligns it — it renders a block-level span, so it reads as its own narrow column inside the wider text.

Quotes and lists

Content is only as portable as the fields that describe it — a quote block still means 'quote' when the design changes.
  • Bulleted list item
  • A second bullet, to show item spacing
  1. Numbered list item
  2. A second numbered item
  1. Lower-alpha list item

Code blocks

A fenced code block is its own type, so it can sit inside a rich-text field like this one or as a plain field on any schema that wants a snippet. Highlighting is resolved at build time and carries both colour schemes, so a block recolours with the site rather than staying dark on a light page. The inline code decorator above is a different thing entirely.

FeatureGrid.vueVue
<template>
    <PageSection :options="component?.options">
        <h2 v-if="component?.heading">{{ component.heading }}</h2>
    </PageSection>
</template>

<script setup lang="ts">
import type { FeatureGrid, ComponentProps } from '~~/types'

interface Props extends ComponentProps<FeatureGrid> {}
defineProps<Props>()
</script>

Set a block to "Use code colours" instead of "Inherit page theme" when the snippet should keep the standard dark code styling whatever the page around it is doing.

Shell
npm run dev:all        # Nuxt on 3000, Studio on 3333
npm run test:run       # unit + Nuxt tests
npm run generate       # static build