CSS is Awesome

← All posts

engineering

From 0.8 to 1.0

We cut 1.0 on 2026-08-17 with 24 of 42 planned stories done and an entire epic unstarted. Here is the reasoning, and what actually changed.

Jerry Hansen · · 8 min

  • release
  • semver
  • versioning

The v1.0.0 commit changed seven files. None of them were library code.

CHANGELOG.md    CONTRIBUTING.md    README.md    SECURITY.md
package.json    docs/faq    docs/install

That is the honest shape of this release. 253610a bumped package.json from 0.8.2 to 1.0.0, repointed the CDN URLs from @0.8 to @1, and rewrote a support table. Zero SCSS touched. The interesting question is not "what did 1.0 add" — it is "why call it 1.0 at all."

The version number was the last thing to change

The real break happened on 2026-05-21, in 2e8a3c6 — the v0.8 mixin-first reframe. That commit is where cia stopped being a class library that also had mixins and became a mixin library where the selector is your problem, not ours.

It was not gentle. Twelve mixin renames with no aliases and no deprecation window:

// 0.7
@include m.bp(md) { … }
@include m.inset(3);

// 0.8 — hard cut, no alias shipped
@include cia.media(md) { … }
@include cia.pad(3);

The responsive : utilities (.cia-sm:flex and friends) were deleted outright — 85 lines. Theme selectors collapsed: [data-theme="press-light"] became [data-theme="press"], with both modes handled by light-dark() inside one file. Utility classes went opt-in behind @use … with ($utilities: true).

The payoff was measurable. main.scss compiled output went from 16.6 KB gzipped to 8.2 KB — half the bytes, mostly by deleting things.

The cost was equally real: anyone on 0.7 had to do a find-and-replace across their whole codebase with no compiler-assisted migration path. We wrote MIGRATION.md instead of shipping aliases, on the theory that a deprecation shim you never remove is worse than a rename you do once. That theory is only cheap because of the next section.

Nobody was using it

The package has never been published to npm. Not once, not as a prerelease.

This is the fact that makes the whole 1.0 decision legible. A SemVer promise costs you exactly as much as the number of people it constrains. With zero external consumers, the promise costs nothing today and buys the thing we actually wanted: a fixed reference point so the docs, the MCP server, and the CDN URLs stop describing a moving target.

It also means the @1 CDN URLs in /docs/install currently 404. We shipped them anyway, documented as such in the release commit, because the alternative was leaving @0.8 links that would be wrong the moment we do publish.

Cutting at 24 of 42

The v1.0 epic backlog was locked 2026-05-23 with 42 stories across five epics. The last audit before the release, 2026-07-16, put it at 24 shipped (roadmap/epics/v1-0/README.md):

Epic Stories State at cut
Recipes book 13 7 — schema, catalog, dynamic route, MCP exposure; 2 of 5 planned recipes
Theme editor polish 9 5 — share URL, name, .css download
Migration on-ramp 6 6 — done
Playground 7 0 — no /playground route exists
Bug fixes + MCP polish 7 5 — MCP test suite not built

An entire epic at zero. The playground — paste SCSS, see it render, share the URL — is the single most useful thing we could build for someone evaluating cia, and it is not in 1.0.

The trade-off, stated plainly: 1.0 here means "the API is stable," not "the product is finished." Those are different claims and conflating them is how version numbers become marketing. The mixin signatures, the 123-token contract, and the theme architecture are what SemVer now protects. The website is not part of the versioned surface — VERSIONING.md names exactly three: CSS class names, the SCSS mixin API, and the token contract. The playground can ship in 1.1 without lying to anyone.

What actually shipped in the 0.9/1.0 window

Between the 0.8 reframe and the version bump, roughly eighty commits:

A zero-emit authoring barrel (e76dede). The documented import for component styles, @use 'css-is-awesome' as cia, resolved to the CSS-emitting bundle — no mixin API, and it printed :root, which Next.js CSS Modules pure mode rejects. The docs had been teaching an import that did not compile.

@use 'css-is-awesome/api' as cia;   // forwards everything, emits nothing
.card { @include cia.card-base($shadow: 2); }

That work also caught a latent bug: spinner() and skeleton() declared their @keyframes at module top level, so CSS Modules renamed the keyframes but not the reference. They now emit via @at-root inside the mixin, only when called.

A migration CLIcia migrate tailwind and cia migrate bootstrap, four PRs (#6, #7, #8), parsing a Tailwind config or Bootstrap _variables.scss into a cia theme with a per-mapping confidence report. The only epic that finished.

Print/PDF as pure CSS (47f1fc2). Four mixins — print, print-base, print-hidden, print-only — and three custom properties. No Puppeteer, no server: the browser's Print → Save as PDF is the generator. Pulled forward from v1.2 because it was small and the recipes book needed the win.

MCP server to 30 tools. Also, in 8b54516, SERVER_VERSION stopped being a hardcoded '0.8.2' literal.

Version is derived, not typed

Two places reported the version. Both were string literals. Both had already drifted — the homepage hero said v0.8 while the package said 0.8.2.

// src/app/page.tsx
import { version } from "../../package.json";
// mcp/server.cjs
return require(path.join(PROJECT_ROOT, 'package.json')).version;

Both fixed on 2026-08-17, hours before the release. The verification note in the release commit reads: hero renders "v1.0.0 · Mixin-first", MCP serverInfo returns {"name":"css-is-awesome","version":"1.0.0"}. Nobody typed either one.

The two weeks before the cut were mostly finding things that were wrong

Three worth naming, because they are the kind of bug that reports success.

The a11y validator was blind (34edf0f). cia fails builds on contrast violations by default. But scripts/theme-a11y.js could not parse light-dark() — so seven themes scored 0 of 17 contract pairs, 119 silent skips, while printing a green check and exiting 0. The fail-by-default guarantee had not been running on the flagship themes. Fixed with a paren-aware splitter; the audit now evaluates both color schemes per pair and keeps the worse one. All seven came back clean, and the actual defect it had been hiding — recipe chips at 1.49:1 against a 4.5:1 requirement — got fixed the same commit.

The Playwright baselines were stale since 2026-05-03 (652f6b2). Confirmed stale rather than a font artifact, because the images differed in height: the /docs baseline was 2897px against 4308px actual. The job had been failing on every PR since roughly May. Root cause was a missing {platform} key in snapshotPathTemplate, so a linux CI runner and a win32 dev machine shared one baseline set — only one of them could ever be green.

The theme count was inconsistent (6646365, dcff8d8) — different pages said 8 and 9 with nothing explaining the gap. It turns out both are right. scss/themes/ holds 9 sources but they form 8 families, because terminal is authored as two single-mode files — terminal dark-only and terminal-light light-only — while the other seven each carry both modes in one file via light-dark(). Terminal is two brands, not two modes.

So the picker offers 8, public/themes/ builds 21 files, and MCP list_themes reports 9 because it counts sources. The docs now say 8 theme families and explain the exception rather than pretending one of the numbers is wrong. The gallery page, separately, was missing prism entirely — it had been shipping and validating clean while listed nowhere.

Where 1.0 actually stands

Measured, gzipped:

Bundle Size
dist/tokens.css 2.2 KB
core.min.css 2.4 KB
utilities.min.css 4.1 KB
css-is-awesome.min.css (full) 7.3 KB
Per-theme file 1.5–3.4 KB
JavaScript in the package 0 KB

Eight theme families across 21 files. A 123-token contract. A 49-glyph icon core. Six components that need no JavaScript — accordion, modal, tooltip, dropdown, tabs, copy button — built on <details name>, <dialog>, popover, and :has().

What 1.0 did not include: a playground, three of five planned recipes, an MCP test suite, and a published npm package.

One of those closed quickly. 8aab2e7 shipped call-and-assert coverage for both surfaces — 174 of 174 public mixins and functions, 30 of 30 MCP tools, CI-gated at 98%. It found a real bug on its first proper run: scss/_icons.scss referenced a $icon-size the theme barrel never re-exported, which broke svg(), svg-bg(), fa-icon() and fa-spin() whenever they were called without an explicit size. Compiled clean, reported success, and was wrong — the same genre of failure as the a11y validator above.

The playground, the three recipes and the npm publish are still open. The number on the box means one thing only — if a mixin signature changes, the first digit changes with it.

← All posts

Theme