Base elements
elements man style/base Read as markdownBare HTML elements style themselves. Write semantic markup (<h1>, <p>,
<a>, <ul>, <table>, <code>) and it looks right with no classes. This is
base.css in @layer base.
Because base rules sit in the base layer (below components and utilities),
any class you add wins automatically. There's no specificity battle: the layer
order does the work, so you never need !important to override a base style.
Spacing
Flow content is spaced for you. Headings, paragraphs, lists, quotes, tables, figures, forms and rules carry vertical margins, so an ordinary page reads as a document without a single class.
Margins collapse between siblings, so a heading's bottom margin and the
following paragraph's top margin resolve to the larger of the two rather than
adding up. First and last children reset their outer margin, so a padded
container (a .card, a .callout) never gains a phantom band of space inside
its own padding.
The layout primitives space their children with gap instead, so they clear
those margins: .stack, .cluster and .row all zero the flow margins of
their direct children. A gap-based layout never doubles up, and you don't have
to know that to get it right.
The page frame
A <main> with no class of its own gets a measure, a gutter and top and bottom
padding, because a bare document rendered edge to edge at 130 characters a line
is unreadable:
<main>
<h1>A page</h1>
<p>This is already laid out.</p>
</main>
Put a class on the element and the frame steps out of the way, on the
assumption that you have taken over the layout. .page-shell and the app-shell
classes in layout supply their own.
Inside that frame, running text stops at --measure (68ch): paragraphs, list
items, quotes, definitions and captions all cap there, so a wide document still
reads well even though the frame itself is wide enough for a table.
The cap is scoped to the frame on purpose. Capping every paragraph on every page
would reach into your components, where it is the wrong call often enough to
matter and silent when it is: a paragraph in a text-align: center hero keeps
its capped box flush left and renders off-centre from the heading above it. Put
a class on the main and your paragraphs are your own.
Colored containers
Put anything in a container that sets a color and the text inside inherits
it, headings and paragraphs included:
<div style="background: #0b1f30; color: white">
<h2>Readable</h2>
<p>So is this.</p>
</div>
This works because base rules set color on almost nothing. html and body
supply --ink for the document, and everything below them inherits. Only
elements that carry their own background set a color of their own (code,
pre, kbd, mark, ::selection). Those own both sides of the pairing, so
they stay legible instead of floating with the surroundings.
Elements that are meant to read softer than body text (dd, blockquote,
th, figcaption) use --ink-soft-flow, which is derived from
currentColor. They stay soft on a normal page and still follow the container
on a colored one.
Links adapt too, taking their lightness from the text around them. See links below.
Document
html and body set the sans font, base font size, body line height, --ink
text, and --bg background. Dark mode flips both through the tokens.
Headings
h1–h6 form a modern scale, with semibold, tight leading, tightened
tracking on the larger sizes:
| Element | Size |
|---|---|
h1 |
--text-4xl (48px) |
h2 |
--text-3xl (36px) |
h3 |
--text-2xl (30px) |
h4 |
--text-xl (24px) |
h5 |
--text-lg (20px) |
h6 |
--text-base (16px) |
Links
Bare <a> renders as an underlined link with a soft underline that solidifies
on hover and a focus ring on :focus-visible. The rule is wrapped in
:where() so link component classes (.button, .link-action, and the like)
override it without a specificity fight, even within the same layer.
The color adapts to the surface. A link takes its lightness from the text
around it, nudged a fifth further from mid-grey so it reads as a link rather
than as a tinted word, and its hue and chroma from --link-c and --link-h.
The result is readable on a white page, on a dark panel and on a saturated
brand hero with no class and no per-surface tokens:
<div style="background: #0b1f30; color: white">
<p>A <a href="/">link</a> here is light, because the text around it is.</p>
</div>
A fixed color cannot do that. Measured against a dark panel, a pinned accent lands at 1.21:1 in the default theme, which is invisible.
The default theme leaves --link-c and --link-h unset, so links render as
the surrounding ink a shade deeper, plus the underline. That is the monochrome
default and it is always legible. Set both to brand your links; see
branding.
Focus
Anything focusable shows a ring. Links, buttons, inputs, summary and the
interactive components each draw their own; everything else, including an
element you make focusable with tabindex, falls back to a 2px --focus
outline from the base layer. You cannot ship a focusable control with no
visible focus state by forgetting to style one.
Forms
A bare form is a stack of labels and controls with no wrapper, so base spaces
it from the flow: fields are separated, a label sits tight against the
control it names, and adjacent buttons sit side by side rather than touching.
<form>
<label for="email">Email</label>
<input id="email" type="email">
<button type="submit">Sign up</button>
</form>
For validation states, hints, inline errors and grouped controls, use the form components in form.
Lists
A bare ul gets discs, a bare ol gets decimals, both indented, with nested
lists stepping through circle and square. The reset strips markers first so a
nav or a menu built from <ul> starts clean, and the layout primitives strip
them again for exactly that case: <ul class="cluster"> is a row of links, not
a bulleted list.
Definition lists render dt semibold and dd indented in --ink-soft-flow.
Quotes and rules
blockquote gets a left rule and italic --ink-soft-flow text. hr is a single
--rule top border.
Code, pre, kbd
code: monospace, on a--bg-mutedfill with small padding and--radius-xs.pre: monospace block on--bg-inversewith--ink-inversetext,--radius-surface, and horizontal scroll.pre coderesets its own fill so nested code doesn't double up.kbd: a small keycap: monospace, muted fill, a--ruleborder and a subtle bottom shadow.
<pre> uses --bg-inverse, so it flips with the theme (dark block in light
mode, light block in dark mode). The terminal component is different: it stays
dark in both modes. See terminal.
Mark
mark is a highlight using the warning tint (--warning-soft on
--warning-ink).
Tables
table is full-width with small text and collapsed borders. thead gets a
bottom rule, body rows get soft top rules between them, th is left-aligned
semibold --ink-soft-flow, and td is top-aligned with comfortable padding.
figcaption renders as small --ink-soft-flow caption text.
For a more editorial table or long-form content, wrap it in .prose (see
typography).
Selection
::selection tints the highlight with the accent color, so text selection picks
up your brand once you've branded the app.