Component
section
Demo
tokens.css,
base.css and section.css. No scripts.
Source
Copy both files, or run npx nojsui add section.
Browser support
Works across current and earlier versions of every major engine.
| Feature | Baseline | Chrome | Edge | Firefox | Safari | Chrome Android | Firefox Android | Safari iOS |
|---|
Usage
<section class="sk-section" aria-labelledby="pricing-title">
<div class="sk-section__inner">
<h2 id="pricing-title">Pricing</h2>
…
</div>
</section>
<section class="sk-section" data-surface="raised" aria-labelledby="faq-title">
<div class="sk-section__inner" data-measure="reading">
<h2 id="faq-title">FAQ</h2>
…
</div>
</section>
Two elements, and the split is the whole design: the outer element paints edge to edge and owns the vertical rhythm; the inner one is the centred column. Trying to do both on one element is why hand-written sections end up with a wrapper nobody can name.
| Attribute | Where | Effect |
|---|---|---|
data-surface="base" (or omitted) | root | --sk-color-bg |
data-surface="raised" | root | --sk-color-surface, for alternating bands down a page |
data-measure="reading" (omitted → wide) | inner | --sk-size-measure-reading (65ch) instead of --sk-size-measure-wide (72rem) |
Two measures, and why they use different units
--sk-size-measure-wide is 72rem and --sk-size-measure-reading is 65ch.
That is deliberate. A page container is a layout dimension: it should not
stretch when the reader enlarges the typeface, so it is in rem. A reading
measure is a typographic one — a comfortable line is roughly 45 to 75
characters — so it is in ch and tracks whatever typeface is set. Using one
unit for both makes one of them wrong.
Both live in tokens.css, not here, because page width is a page-level
decision — the same reasoning as --sk-density (ADR 0019). Two components each
carrying their own idea of how wide a page is would drift.
Neither of these is --sk-size-measure (90ch), the existing global token
tabs and accordion read for panel/item body copy. That one is a reading
measure too — a deliberately wide one, per ADR 0024 — not a container width;
it just happens to be wider than what section and prose needed. The point
of adding --sk-size-measure-reading was not that 90ch is somehow not a
reading measure, it is that the primitives needed a narrower one and a
separate container width, which is a different problem than the one ADR 0024
solved. See ADR 0027’s amendment to 0024 for the full reasoning.
Theming
| Property | Default | Controls |
|---|---|---|
--sk-section-padding-block | --sk-space-3xl | Vertical rhythm between bands |
--sk-section-padding-inline | --sk-space-lg | Gutter, so the inner column never touches the edge |
--sk-section-bg | --sk-color-bg | Band background (--sk-color-surface when raised) |
--sk-section-measure | --sk-size-measure-wide | Inner column width (--sk-size-measure-reading when data-measure="reading") |
Keyboard contract
None — it is layout. Whatever you put inside keeps its own behaviour.
Accessibility notes
<section>is only exposed as aregionlandmark when it carries an accessible name —aria-labelledbypointing at the band’s heading, oraria-label. A heading nested inside a<section>does not name it; a<section>with noaria-labelledby/aria-labelis not a landmark at all, it is just an element, and screen-reader users get no benefit from it over a<div>. So the real choice is between wiring uparia-labelledby(as the demo does) and using a plain<div class="sk-section">for a band that is only visual.- No motion, no state, nothing else to announce.
- The gutter is
padding-inline, so it flips correctly in RTL without a second rule. data-surface="raised"changes the background only. Text colour comes from the inherited--sk-color-text, which is contrast-checked against both surfaces bytools/tokens/check-contrast.mjs.
Degradation
Baseline column from support.json — regenerate with pnpm support.
| Feature used | Baseline status | Behavior without it |
|---|---|---|
| Logical properties | widely | Nothing to degrade. padding-block, padding-inline and max-inline-size have been Baseline widely available since 2021. |
There is no @supports block here and nothing that any supported engine can
miss. A primitive should be the piece everything else can assume.