Heading

h1…h6 by level, sized by the base layer.

Picks the element, nothing else. level rendersh1h6, and the appearance comes from the base layer, which already maps each level to a Utopia step, a weight and its rhythm. Separating the two is the point: a section that needs to sit ath3 in the document outline can say so without anybody arguing about how large it should look.

Which is why almost every block takes a headingLevel prop and passes it straight through. The host owns the outline, because only the host knows what the page above the block looks like.

Because it has no <style> of its own, a parent's scoped rule does not reach the element it renders — a passed classarrives, the scope hash does not. Use :global() under a scoped ancestor, or a class from the components layer such as.section-h. This is the most expensive gotcha in the repo and it looks exactly like a specificity bug.

The six levels

Tone

Every home comes with a garden

Every home comes with a garden

Every home comes with a garden

Every home comes with a garden

Every home comes with a garden
Every home comes with a garden

Size held, level changed

All three are .section-h from the components layer, ath2, h3 and h4. They look identical and the outline reads correctly — that is the separation working.

Tone

A section heading at level 2

A section heading at level 3

A section heading at level 4

Props

packages/primitives/src/Heading.astro
PropTypeRequired
level1 | 2 | 3 | 4 | 5 | 6
classstring