Manual Style Base elements

Base elements

elements man style/base Read as markdown

Bare 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

h1h6 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-muted fill with small padding and --radius-xs.
  • pre: monospace block on --bg-inverse with --ink-inverse text, --radius-surface, and horizontal scroll. pre code resets its own fill so nested code doesn't double up.
  • kbd: a small keycap: monospace, muted fill, a --rule border 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.