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
- Tokens with today's values as defaults (zero diff):
- Agave on tokens (accepted visual change, editor only):
sub and sup rules, with a reset-independence test.
- Optional: derive the heading sizes from
--content-scale, and document presets. Visual change, decided separately.
Open questions
- 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.
- Heading sizes: explicit per-level tokens only, or derived from
--content-scale by default? Explicit is easier to read; derived prevents ordering bugs.
- 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?
- Agave's body text: does
font-thin stay a theme choice, as --content-weight, or go?
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 existingcontent.cssrules 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 theplone-contentlayer, 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
.slate-h1and.slate-titleread--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.--heading-font-familyand uses it inpublicui.cssrules on bareh2/h3, which only load in the Public UI.--text-base: 1.15remandtext-base leading-6on.content-area, also Public UI only.--block-bottom-spacingper block category (calc(var(--spacing) * 3)for text blocks), and fixed per-heading margins incontent.css(1.6em,1.4em,1em,0.75em). Nothing derives from one value.Problems this causes
publicui.css(Block content: settle the visual quirks kept for zero diff in #200 #227, item 4).--text-baseto 1.15rem (18.4px) for body text, and.slate-h6happens to read--text-base, while h5 reads--text-lg(18px). Raising the site's base text size reshuffles the heading order because the headings read the UI's global type scale.<sub>and<sup>have nocontent.cssrules, so their size and position depend on the reset (Remove Tailwind from block content in @plone/plate and @plone/blocks #200's rule: content must not rely on one).What Typeset does, and why we shouldn't ship it as is
Typeset is one CSS file that styles HTML inside a
.typesetwrapper. Everything derives from three rhythm variables,--typeset-size,--typeset-leadingand--typeset-flow, and three fonts,--typeset-font-body,--typeset-font-headingand--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-docsand.typeset-chat, andnot-typesetopts a subtree out.#156 tried the file itself. It doesn't fit Aurora's content model:
components, so it loses to thebasereset and sits below the content layer. Aurora's content styles belong inplone-content.h2andpinside the wrapper, including those inside Plone blocks (teaser titles, listing items) and editor chrome. Each would neednot-typeset. Aurora's rules target Plate nodes (.slate-h2), so they already scope themselves to the content.margin-block-start. Aurora spaces them with the inner container'spadding-bottom(--block-bottom-spacing), so the two would add up.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 thecontent.cssrules of@plone/plateand@plone/layout. Defaults are today's values, so nothing changes until a theme sets a token.--content-font-body--content-font-heading--content-font-bodyh1–h6--content-font-mono--font-monoand its stack--content-size--content-leading--content-size--content-flowcalc(var(--spacing, 0.25rem) * 3)--block-bottom-spacingfor text blocks--content-heading-weightvar(--font-weight-semibold, 600)h2–h6--content-heading-trackingvar(--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-basefont-size--content-title-leading,--content-h1-leading…--content-h6-leading--text-*--line-heightline-height--content-*is for content-wide typography;--block-*stays for values that belong to one block, such as--block-callout-background.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:
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:
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.csstoagave/styles/content.css, as token values instead of bareh2/h3rules with@apply. The editor then shows the same typography as the public view. This replaces #227's item 4.Gaps to close along the way
subandsup: addcontent.cssrules (size,vertical-align,line-height) so they don't depend on the reset, and cover them in a reset-independence test.--text-base.Documentation
Already available
.content-areaversus overriding rules, every block's classnames, and the current tokens. Its "Theme values" section lists the Tailwind values the content reads today, such as--text-smand--font-weight-bold. This proposal replaces those for typography.plone-contentlayer and the authoring rules forstyles/content.css.slate-<type>,block-<type>__<part>, data-attribute variants).styles/content.css".To add: a "Theme the typography" section in "Style blocks in a theme"
A draft of the section, to land with the tokens:
Phases
content.cssreads the new tokens, with the current values as fallbacks.content.cssas token values, and its bareh2/h3and.content-areatypography rules leavepublicui.css.subandsuprules, with a reset-independence test.--content-scale, and document presets. Visual change, decided separately.Open questions
--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.--content-scaleby default? Explicit is easier to read; derived prevents ordering bugs.--content-flowbecome the default for every category's--block-bottom-spacing, with the heading and action categories as multiples of it, or only for text blocks?font-thinstay a theme choice, as--content-weight, or go?