Baseline.Tokens.Layouts.Behaviors.
A way of structuring CSS so a product stays in step with its design system while both keep changing. Semantic HTML is styled before it has a class, components ask for meanings rather than values, space comes from named patterns, and state lives in attributes.
npm install @btlb/cssBuild it one layer at a time.
One small piece of HTML, four steps. Each layer does one job and builds on the one before, and what you change early carries through to the end. It runs the real published CSS.
See the HTML
<article class="stack" data-gap="5">
<header class="stack" data-gap="2">
<h2>Specimen 01</h2>
<p>A drawing’s title block, in plain semantic HTML.</p>
</header>
<dl class="cluster" data-gap="8">
<div><dt><small>Scale</small></dt><dd>1:1</dd></div>
<div><dt><small>Sheet</small></dt><dd>1 of 1</dd></div>
<div><dt><small>Drawing</small></dt><dd><code>spec-01</code></dd></div>
</dl>
<div class="cluster" data-gap="3">
<button type="button" aria-pressed="false" data-state="idle">Approve</button>
<p role="status">Draft</p>
</div>
</article>Stop making the same small decisions.
Most styling is built one decision at a time, each made again on the next screen. The four layers give every one of them a standing answer.
“Does this heading need its own size and margin?”
Baseline
<h2>is already styled, from tokens.“What was that hex code for the border?”
Tokens
var(--color-border-default)names the meaning; the value lives in one place.“I need a flex container here.”
Layouts
class="stack" data-gap="4"names the pattern and configures it.“I’ll add a class for when it’s active.”
Behaviors
data-state="active"is the state, readable by CSS, scripts and assistive technology.
You stop making small visual decisions everywhere and start assembling meaningful parts.
Baseline: Bare HTML already looks like the system.
Baseline styles the elements themselves: headings, text, links, code and form controls, all from semantic tokens. Plain semantic HTML looks finished before it has a single class, and every component starts from something that already looks right. Switch between the browser’s defaults and the published layer.
<article>
<h2>Release notes</h2>
<p>Exports keep their <a href="#">original file names</a>, and <code>--dry-run</code> lists what would change.</p>
<label for="notes-email">Email address</label>
<input id="notes-email" type="email" value="[email protected]">
<label for="notes-when">Send release notes</label>
<select id="notes-when"><option>For every release</option><option>Once a month</option></select>
<button type="button">Save</button>
</article>No classes · component CSS: none · the space between elements comes from Layouts
Tokens: Change what a value means, in one place.
Tokens come in two tiers. Primitives are the raw scales: a grey, a step of spacing. Semantic tokens say what a value is for: a border, a card’s padding, disabled text. Components use only the semantic tier, so a design change is a new mapping rather than a search. Try three ordinary requests on the same panel written both ways.
Hand-written CSS
Every value is typed where it is used: 9 colour values in this one panel.
* { margin: 0; box-sizing: border-box; }
body { background: #0b131d; font-family: 'Archivo Variable', sans-serif; line-height: 1.5; color: #eef2f7; }
.panel { padding: 24px; background: #172339; border: 1px solid #3b4d69; }
h3 { font-family: 'Poppins', sans-serif; font-size: 24px; line-height: 1.25; margin-bottom: 8px; }
.row { display: flex; justify-content: space-between; align-items: center; gap: 24px; flex-wrap: wrap;
padding-block: 16px; border-top: 1px solid #3b4d69; }
.label { font-weight: 500; }
.help { font-size: 14px; color: #b4bfcf; }
.help--disabled { color: #3b4d69; }
button { padding: 8px 16px; border: 0; border-radius: 6px; font: inherit; font-weight: 500;
line-height: 1.25; background: #eef2f7; color: #101a27; cursor: pointer; }
button:disabled { opacity: 0.5; cursor: not-allowed; }With the four layers
The component names meanings. The values live in the semantic tokens.
body { background: var(--color-bg-sunken); }
.panel { padding: var(--spacing-card-padding); background: var(--color-bg-surface);
border: 1px solid var(--color-border-strong); }
.panel h3 { margin-bottom: var(--spacing-stack-gap-tight); }
.row { padding-block: var(--spacing-stack-gap-normal); border-top: 1px solid var(--color-border-strong); }
.label { font-weight: var(--typography-label-font-weight); }
.help { font-size: var(--typography-label-font-size); color: var(--color-text-secondary); }
.help--disabled { color: var(--color-text-disabled); }Both panels are live and start identical. The hand-written one loads none of the layers; the other loads the four published layers and this site’s tokens. A request runs as a literal find-and-replace on the left and a single token change on the right.
One token, from design file to button.
Source. W3C design token JSON, in
packages/tokens/tokens/semantic/colors.json."interactive": { "$value": "{color.blue.600}", "$description": "Primary interactive elements (buttons, links)" }Build. Style Dictionary turns references into custom properties inside
@layer tokens.--color-bg-interactive: var(--color-blue-600);Use. Baseline styles every
<button>with the semantic name, never the primitive.button { background: var(--color-bg-interactive); }Remap. This page points the same name at its own navy palette. No Baseline rule changed.
--color-bg-interactive: var(--color-btlb-ink);
Fluid tokens handle values that differ between a phone and a desktop: the range is written once withclamp(), and a value that belongs to a box rather than the screen uses container units, so components never think about breakpoints. A bridge token switched in a media query is left for a value that must jump.Fluid and bridge tokens in the guide.
Layouts: Name the pattern once.
The class says what a grouping is for. Data attributes configure it. Tokens supply the values. Read the markup and you know why the elements sit together, and when the design system changes its spacing, the pattern changes once. Every option below is read from the 1.1.0 source.
The patterns respond to the space they get, so narrow the frame to see them wrap.
<div class="stack">…</div>Behaviors: State is an attribute anyone can read.
Your script, in any framework, only sets attributes: data-state, and the ARIA attribute that means the same thing. The stylesheet draws the state and assistive technology announces it, from the same markup. Nobody has to decode .is-active-v2.
- What a screen reader announces for the button
- Save changes, button
Announcements are typical wording; each screen reader phrases them its own way.
<div class="stack" data-gap="4" data-align="start">
<button type="button" data-state="idle">Save changes</button>
<p role="status" class="message" data-state="info">Changes are saved to this workspace only.</p>
</div>button.dataset.state = 'idle';The 1.1.0 behaviors layer responds to 9 states (loading, success, error, warning, info, active, disabled, idle, hidden) and 4 motions. Under prefers-reduced-motion: reduce, every element with a data-motion attribute drops its transitions. Behavior reference.
What wins is decided once.
Each layer lives in its own cascade layer, declared in a single line: @layer tokens, baseline, layouts, behaviors;. A rule in a later layer beats one in an earlier layer, however specific its selector, so a state never loses to a heavy selector in Baseline and nobody reaches for !important.
The name follows how you build, starting from the HTML. The cascade puts tokens first, because that layer only holds values the other three read.
@layer tokens, baseline, layouts, behaviors;
@layer baseline { #note.note::after { content: "baseline wins"; } }
@layer behaviors { [data-state="active"]::after { content: "behaviors wins"; } }Behaviors wins. A later layer beats an earlier one whatever the selectors weigh.
Components assemble the layers.
A component isn’t a fifth layer. A card is Baseline typography, semantic tokens for its surface, layout patterns inside, and behaviors for its states. When card padding changes, the token is remapped; when stacks get new spacing, the pattern changes. The component’s own CSS rarely does.
A smaller vocabulary for generated code.
Strict layers suit AI coding tools for the same reason they suit people: named tokens instead of arbitrary values, named patterns instead of ad-hoc flex and grid, attributes instead of invented class names. The output stays consistent with the system. For how an agent should plan and prove its work, rather than what CSS it writes, see FeatureKit.
- BaselineTypography and button defaults.
- TokensIts surface, border, padding and gap, as semantic tokens.
- Layouts.stack and .cluster for its internal structure.
- BehaviorsHover, focus, active, loading and error states.
Adopt it one layer at a time.
None of this needs a rewrite. Adoption runs in a different order from the name, starting where the payoff is quickest, and each step is useful on its own.
- 01Start with tokens
Split your existing CSS variables into primitives and semantics, and stop hardcoding values in components.
- 02Add Baseline
Style semantic HTML elements with your tokens so bare markup looks right.
- 03Introduce layout patterns
When the same flex or grid recipe repeats, extract it into a named pattern.
- 04Move state into attributes
Replace state class names with data-state and aria-* attributes as you touch components.
Install
npm install @btlb/css/* Declares the layer order, then imports all four */
@import '@btlb/css';Or take layers one at a time. Baseline, Layouts and Behaviors read tokens, so tokens come first.
@btlb/css | 1.1.0 |
|---|---|
@btlb/tokens | 1.1.0 |
@btlb/baseline | 1.1.0 |
@btlb/layouts | 1.1.0 |
@btlb/behaviors | 1.1.0 |
What 1.1.0 covers.
Counted from the source at commit ce65475, so this list moves when the packages do.
- Tokens
- 132 primitives and 66 semantic tokens in W3C DTCG JSON, built with Style Dictionary. The semantic tier covers colour, spacing and typography; radii, shadows and motion are used as primitives for now.
- Baseline
- A reset, typography (headings, text, links, code, quotes) and form controls (buttons, inputs, selects, checkboxes, fieldsets).
- Layouts
- 6 patterns: .stack, .cluster, .grid, .sidebar, .center, .switcher. The README names
.frameand.reelas following the same convention; they aren’t in the 1.1.0 package. - Behaviors
- 9 states, 4 motions with a reduced-motion fallback, focus and skip-link styles, and high-contrast and reduced-transparency adjustments.
The repository is the reference.
The docs are generated from the repository at ce65475, so they describe the version you install. For tools, all of it as one text file.
- The guideThe full methodology, from the repository README.
- @btlb/tokensThe two tiers, overriding them, and bringing your own W3C tokens.
- Token referenceAll 198 tokens, generated from the token JSON.
- Layout referenceEvery pattern and attribute, generated from the CSS.
- Behavior referenceStates, motion and the ARIA hooks, generated from the CSS.
- Source on GitHubThe packages, the token JSON and the Style Dictionary build.