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.

Terminal
npm install @btlb/css

Build 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.

Four layers, each with one job: Baseline styles the elements, Tokens hold every value, Layouts set the space and Behaviors show the state.
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.

  1. “Does this heading need its own size and margin?”

    Baseline

    <h2> is already styled, from tokens.

  2. “What was that hex code for the border?”

    Tokens

    var(--color-border-default) names the meaning; the value lives in one place.

  3. “I need a flex container here.”

    Layouts

    class="stack" data-gap="4" names the pattern and configures it.

  4. “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.

From the README

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.

  1. Source. W3C design token JSON, in packages/tokens/tokens/semantic/colors.json.

    "interactive": {
      "$value": "{color.blue.600}",
      "$description": "Primary interactive elements (buttons, links)"
    }
  2. Build. Style Dictionary turns references into custom properties inside @layer tokens.

    --color-bg-interactive: var(--color-blue-600);
  3. Use. Baseline styles every <button> with the semantic name, never the primitive.

    button {
      background: var(--color-bg-interactive);
    }
  4. 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.

Vertical rhythm

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.

Button
Message
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.

Card · what each layer supplies
  1. BaselineTypography and button defaults.
  2. TokensIts surface, border, padding and gap, as semantic tokens.
  3. Layouts.stack and .cluster for its internal structure.
  4. 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.

  1. 01
    Start with tokens

    Split your existing CSS variables into primitives and semantics, and stop hardcoding values in components.

  2. 02
    Add Baseline

    Style semantic HTML elements with your tokens so bare markup looks right.

  3. 03
    Introduce layout patterns

    When the same flex or grid recipe repeats, extract it into a named pattern.

  4. 04
    Move state into attributes

    Replace state class names with data-state and aria-* attributes as you touch components.

Install

Terminal
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.

Packages and versions
@btlb/css1.1.0
@btlb/tokens1.1.0
@btlb/baseline1.1.0
@btlb/layouts1.1.0
@btlb/behaviors1.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 .frame and .reel as 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.