Skip to content

Typography tokens for block content, inspired by shadcn Typeset #241

Description

@sneridagh

Summary

Give block content a small set of typography tokens that themes set on .content-area: fonts, base size, line height, vertical rhythm, and a heading scale. The model is shadcn's Typeset, but we adopt its philosophy, not its CSS file: the tokens feed the existing content.css rules from #200. Today's values become the defaults, so introducing the tokens is zero-diff.

Background

#200 moved block content from Tailwind utilities to plain CSS in each package's styles/content.css, in the plone-content layer, with zero-specificity rules and no reliance on a reset. Theming block colors and spacing is documented in Style blocks in a theme. Typography is the part that's still hard to theme, because the rules read Tailwind's global type scale directly.

How content typography is themed today

A theme wants to… Today
Change the heading sizes Only indirectly. .slate-h1 and .slate-title read --text-4xl, .slate-h2 --text-2xl, .slate-h3 --text-xl, .slate-h4/.slate-h5 --text-lg, .slate-h6 --text-base. A theme has to know which step each heading uses, and the steps are shared with the rest of the UI.
Change the heading font No framework token. Agave sets --heading-font-family and uses it in publicui.css rules on bare h2/h3, which only load in the Public UI.
Change the body size and line height Paragraphs don't set either: they inherit from the page. Agave sets --text-base: 1.15rem and text-base leading-6 on .content-area, also Public UI only.
Change the vertical rhythm Two separate systems: layout's --block-bottom-spacing per block category (calc(var(--spacing) * 3) for text blocks), and fixed per-heading margins in content.css (1.6em, 1.4em, 1em, 0.75em). Nothing derives from one value.
Offer a denser or larger-text variant Not possible without rewriting the rules.

Problems this causes

What Typeset does, and why we shouldn't ship it as is

Typeset is one CSS file that styles HTML inside a .typeset wrapper. Everything derives from three rhythm variables, --typeset-size, --typeset-leading and --typeset-flow, and three fonts, --typeset-font-body, --typeset-font-heading and --typeset-font-mono. Heading sizes, the gap under a heading, list indents and the space around rules all follow from them. Presets are classes that reset those variables, such as .typeset-docs and .typeset-chat, and not-typeset opts a subtree out.

#156 tried the file itself. It doesn't fit Aurora's content model:

  • Cascade layer: it lives in components, so it loses to the base reset and sits below the content layer. Aurora's content styles belong in plone-content.
  • Element selectors: it styles every h2 and p inside the wrapper, including those inside Plone blocks (teaser titles, listing items) and editor chrome. Each would need not-typeset. Aurora's rules target Plate nodes (.slate-h2), so they already scope themselves to the content.
  • Spacing direction: it spaces blocks with margin-block-start. Aurora spaces them with the inner container's padding-bottom (--block-bottom-spacing), so the two would add up.
  • Concerns Aurora doesn't share: streaming stability matters for chat output, not CMS content. Leaving the maximum width to the layout is already how Aurora works.

What's worth adopting is the token model: a handful of semantic values on the content root, with everything else derived from them, and presets as classes.

Proposal

Tokens

Set on .content-area, read by the content.css rules of @plone/plate and @plone/layout. Defaults are today's values, so nothing changes until a theme sets a token.

Token Default (today) Used by
--content-font-body The page's font, inherited The content root
--content-font-heading --content-font-body Title and h1–h6
--content-font-mono --font-mono and its stack Inline code, keyboard input, code blocks
--content-size The page's size, inherited Paragraphs, lists, blockquotes, table cells
--content-leading The page's line height, inherited Same as --content-size
--content-flow calc(var(--spacing, 0.25rem) * 3) The default --block-bottom-spacing for text blocks
--content-heading-weight var(--font-weight-semibold, 600) h2–h6
--content-heading-tracking var(--tracking-tight, -0.025em) h2–h6
--content-title-size, --content-h1-size … --content-h6-size --text-4xl, --text-4xl, --text-2xl, --text-xl, --text-lg, --text-lg, --text-base Each level's font-size
--content-title-leading, --content-h1-leading … --content-h6-leading The matching --text-*--line-height Each level's line-height
  • Naming: --content-* is for content-wide typography; --block-* stays for values that belong to one block, such as --block-callout-background.
  • Heading margins stay in em, so they scale with each heading's size automatically, as Typeset does.

Heading scale

The per-level tokens make every heading independently overridable. As a second step, their defaults can derive from one ratio, which makes the h5/h6 kind of inversion impossible:

:where(.content-area) {
  --content-scale: 1.25;
  --content-h6-size: var(--content-size);
  --content-h5-size: calc(var(--content-size) * var(--content-scale));
  --content-h4-size: calc(var(--content-h5-size) * var(--content-scale));
  /* … */
}

That changes how headings look, so it's a separate, deliberate step.

Presets

A preset is a class that resets a few tokens, as in Typeset:

.content-area.is-compact {
  --content-flow: calc(var(--spacing) * 2);
  --content-leading: 1.4;
}

.content-area.is-large-text {
  --content-size: 1.25rem;
  --content-leading: 1.7;
}

How a preset gets applied (site setting, content field, or theme-only) is out of scope for the first phases.

Agave

Agave's typography moves from publicui.css to agave/styles/content.css, as token values instead of bare h2/h3 rules with @apply. The editor then shows the same typography as the public view. This replaces #227's item 4.

/* packages/agave/styles/content.css */
.content-area {
  --content-font-body: 'Funnel', sans-serif;
  --content-font-heading: 'Funnel Display', serif;
  --content-size: 1.15rem;
  --content-leading: 1.5rem;
  --content-heading-weight: var(--font-weight-normal);
  --content-title-size: var(--text-5xl);
  --content-h2-size: var(--text-3xl);
  --content-h3-size: var(--text-2xl);
}

Gaps to close along the way

  • sub and sup: add content.css rules (size, vertical-align, line-height) so they don't depend on the reset, and cover them in a reset-independence test.
  • h5 versus h6 under Agave: fixed by giving h6 its own content token, so it no longer reads the UI's --text-base.

Documentation

Already available

To add: a "Theme the typography" section in "Style blocks in a theme"

A draft of the section, to land with the tokens:

## Theme the typography

Block content takes its typography from a few tokens on `.content-area`.
Set them in your add-on's `styles/content.css`, and they apply in both the Public UI and the editor.

```css
/* my-theme/styles/content.css */
.content-area {
  --content-font-body: 'Source Serif 4', serif;
  --content-font-heading: 'Inter', sans-serif;
  --content-size: 1.125rem;
  --content-leading: 1.7;
  --content-flow: 1rem;
}
```

- `--content-size` and `--content-leading` set the text of paragraphs, lists, blockquotes, and table cells.
- `--content-flow` sets the space between blocks. Heading margins are in `em`, so they follow each heading's size.
- Each heading level has its own size and line height, from `--content-title-size` and `--content-h1-size` to `--content-h6-size`. Set one to change that level only.
- `--content-heading-weight` and `--content-heading-tracking` set the weight and letter spacing of `h2` to `h6`.

To offer a variant, such as denser or larger text, define a class that resets a few tokens, and add it to the content root:

```css
.content-area.is-large-text {
  --content-size: 1.25rem;
  --content-leading: 1.75;
}
```

Don't set font sizes on `h2` or `p` directly: the tokens keep the editor and the public view consistent, and they keep the heading levels in order.

Phases

  1. Tokens with today's values as defaults (zero diff):
  2. Agave on tokens (accepted visual change, editor only):
  3. sub and sup rules, with a reset-independence test.
  4. Optional: derive the heading sizes from --content-scale, and document presets. Visual change, decided separately.

Open questions

  1. Naming:
    • --content-* (proposed): content-wide, and distinct from per-block --block-*;
    • --typeset-*: signals the lineage, but suggests compatibility with shadcn's file;
    • --block-*: one namespace, but blurs content-wide and per-block values.
  2. Heading sizes: explicit per-level tokens only, or derived from --content-scale by default? Explicit is easier to read; derived prevents ordering bugs.
  3. Rhythm: should --content-flow become the default for every category's --block-bottom-spacing, with the heading and action categories as multiples of it, or only for text blocks?
  4. Agave's body text: does font-thin stay a theme choice, as --content-weight, or go?

Activity

  1. sneridagh commented on Oct 7, 2026

    @sneridagh
    MemberAuthor

    @pnicolli to consider

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions