Skip to content

About

A mixin-first SCSS design system. Write your own selectors and call cia mixins: 24 themes, 32 framework-agnostic recipes, a 33-tool MCP server so AI agents read the real API, WCAG-graded themes, and zero runtime JavaScript in the package.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

875 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

css-is-awesome

A tiny, mixin-first SCSS design system with one-file theme swap.

npm CI Node License: MIT semantic-release

Bring your own components. Bring your own selectors. We bring the design system. No component library to fight, in React, Vue, Angular, Svelte, Web Components, Razor, SharePoint, or plain HTML — cia styles the markup you already own. One CSS file per theme — drop it in and the page restyles, no markup change. 24 themes. Zero runtime JavaScript — nothing in the package is loaded by a page. Six browser-native interactive components. Small enough to read in an afternoon.

Docs: cssisawesome.com · Install: npm install css-is-awesome

The recipes book: build any component in any framework using cia mixins — 32 recipes today, including dialog, command-palette, combobox, combobox-multiselect, datepicker, data-table, pagination, breadcrumb, toast, file-upload, sortable-list, color-picker, app-shell, admin-dashboard-layout, auth-flow, otp-input, multi-step-wizard, confirm-dialog, five form-validation patterns (HTML5, react-hook-form, Zod, async, success-states), three i18n patterns, rtl-layout, print-to-pdf, print-spec, letterhead, mobile-nav and bottom-nav. AI agents read recipes via MCP and generate components in your stack; humans read them at /docs/recipes.

For AI agents — start here

Read llm.txt first. One file, the whole system: install path, hard rules, the mixin vocabulary, and the traps that make agents write wrong cia code. It ships in the npm package, so it's at node_modules/css-is-awesome/llm.txt in any project that has cia.

Then connect the MCP server and stop guessing at signatures. It answers from the real source — 35 tools covering themes, mixins, functions, tokens, recipes, components, theme validation, theme generation from design tokens, the token mapping itself, and a one-call status for version, counts and a surface hash.

{
  "mcpServers": {
    "css-is-awesome": {
      "command": "npx",
      "args": ["css-is-awesome-mcp"]
    }
  }
}

That's the whole setup — css-is-awesome-mcp is a dedicated zero-install package, no manual dependency step. npx fetches and caches it on first run with no install command needed; if you'd rather pin an exact version in your own package.json/lockfile, npm install css-is-awesome-mcp works too — npx then uses the locally installed copy instead of fetching. (The in-repo copy at mcp/server.cjs still works too, if you'd rather not add a second package — see the MCP section below for that path, which needs npm install -D @modelcontextprotocol/sdk zod first since those are optional peers here.)

Why it matters more here than for older frameworks: no model has memorised cia's API the way it has memorised Tailwind's class names. Without llm.txt or MCP, an agent will confidently invent a Tailwind-shaped API. With them, it reads the real thing. Details at /docs/mcp.

Three ways to use it

Full breakdown in THREE-TIERS.md. cia is mixin-first since v0.8 — utility classes are an opt-in convenience.

1. SCSS with mixin API (recommended)

npm install css-is-awesome
npm install -D sass

Or let the wizard do the wiring — it detects your framework, picks a theme, writes the SCSS entry and connects the MCP server, in the project you're already in:

npm create cia@latest        # add --dry-run to see the plan first

Guide: /docs/install/wizard.

@use 'css-is-awesome' as cia;

.checkout-cta { @include cia.btn(primary); }
.faq-item     { @include cia.accordion; }
.modal        { @include cia.modal; }
<!-- One theme file: no data-theme attribute required -->
<link rel="stylesheet" href="/cia/themes/boilerplate/theme.css">

Author your own class names; the mixin handles the styling. Mixins for buttons, forms, layout, typography, color, motion, plus the six zero-JS components: accordion, modal, tooltip, dropdown, tabs, copy-button — plus the mobile navigation family (hamburger, drawer, sheet, dock) and print-to-PDF via a pure-CSS @media print layer. Full reference at /docs/mixins.

Two imports, two jobs. Emit the tokens once from your root/global stylesheet (@use 'css-is-awesome'; or <link> a theme file), then import the zero-emit authoring barrel in each component stylesheet:

// Card.module.scss — emits no :root, safe under Next.js CSS Modules pure mode
@use 'css-is-awesome/api' as cia;

.product-card { @include cia.card-base($shadow: 2); }

Next.js / Turbopack. The barrel works under Turbopack — the showcase app runs Next 16 on Turbopack for both dev and build and imports css-is-awesome/scss/api in every stylesheet.

Sass doesn't read package.json exports, so add node_modules to sassOptions.loadPaths.

One reported hazard, stated with its provenance: a consumer whose own styles directory was on loadPaths hit Two forwarded modules both define a mixin named stack. cia's internal forwards are all ./-relative, and standard Sass resolves those against the importing file without consulting load paths — validate-package asserts the barrel survives a hostile styles/_mixins.scss ahead of node_modules, and it does. So this appears to be a bundler-resolver deviation rather than Sass behaviour. If you hit it, drop your styles directory from loadPaths.

2. Drop-in CSS (zero build)

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/css-is-awesome@1/public/themes/boilerplate/theme.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/css-is-awesome@1/dist/css-is-awesome.min.css">

Theme first (sets the tokens), library second. No data-theme attribute needed — a single theme file styles the page on its own. Swap the URL to swap the theme; the HTML never changes. Bundle tiers — dist/tokens.css (2.2 KB gz, :where(:root) vars only), dist/css-is-awesome.core.min.css (2.4 KB gz, tokens + resets), dist/css-is-awesome.min.css (8.0 KB gz, full).

3. Bare tags (opt-in Pico-mode)

@use 'css-is-awesome/scss/recipes/bare-tags';
<button>Save</button>
<table>…</table>
<input type="email">

One line styles the whole site. Zero classes. Wrapped in :where() (specificity 0,0,0), so any consumer selector — even another bare tag — wins automatically. No @layer, no cascade pollution.

Themes

One file per theme since v0.8. Each theme file emits :root, :root[data-theme="<name>"], so dropping one in restyles the page with no markup change — the data-theme attribute is optional for the single-file case. Set <html data-theme="..."> only when you load the all-in-one bundle (public/theme.css), where every theme shares one file and the attribute is the only thing telling them apart.

8 families, 24 files. Each family ships a dual-mode base (<name>, both light + dark via native light-dark()) plus single-mode <name>-light and <name>-dark variants for pairing.

Theme Mood Modes
boilerplate Neutral slate + clean blue, system fonts, drop-in starter both
sketchbook Warm washi paper / charcoal at night, sumi ink, indigo accent both (brand default)
press Editorial newsprint / night-edition, Playfair serif, press-red both
prism Vercel/Linear/Radix aesthetic, refined blue, neutral grays both
cupertino macOS AppKit, SF Pro, system blue, vibrancy blurs both
glass visionOS glassmorphism, iOS indigo, blur asymmetric per mode both (Pattern C)
graphite Brushed silver / machined dark aluminum, SF system stack both
terminal VT100 phosphor green, zero radii, CRT glow dark-only base (terminal-light is the daylight companion)

Pair two themes per mode with native <link media>:

<link rel="stylesheet" href="/themes/sketchbook-light/theme.css" media="(prefers-color-scheme: light)">
<link rel="stylesheet" href="/themes/terminal-dark/theme.css"    media="(prefers-color-scheme: dark)">

Newspaper by day, hacker terminal by night. No JS, no mixin — pure browser behavior. Most design systems give you dark mode; cia lets you ship a second brand at night. See /docs/themes/pairing.

Each theme is one file of CSS custom properties. Tokens only — no component rules. See public/themes/<name>/theme.css for the compiled output and scss/themes/<name>.scss for the sources. Full contract documented in THEMING.md.

The library's own default tokens emit under :where(:root) (specificity 0,0,0), so any theme declaration outranks them regardless of load order. That's :where(), not @layer — see the print section below for why cia refuses layers.

Edit a theme or make your own

You can. Themes are open files. Edit any token, make brand-new themes, mix and match — cia treats themes as data, not internal magic.

# 1. Copy an existing theme as a starting point
cp scss/themes/boilerplate.scss scss/themes/midnight.scss

# 2. Edit tokens (see scss/themes/*.scss for the pattern + light-dark() usage)
#    Wrap in @include cia.theme('midnight') { ... }

# 3. Build to public/themes/midnight/theme.css (also regenerates public/theme.css)
npm run build:css:themes

# 4. Validate against the token contract + a11y audit
node scripts/theme-validator.js public/themes/midnight/theme.css

# 5. Use it — just link the file. No data-theme attribute needed.
#    <link rel="stylesheet" href="/themes/midnight/theme.css">

A theme can also own the page behind your components: optional hero and band surfaces carry a background, an image or gradient, an ink colour and a scrim, applied with @include cia.surface(hero) or <body data-surface="hero">. Nothing is applied automatically, so adding them never moves a pixel until you ask.

Full authoring walkthrough: /docs/authoring/themes. The contract (127 required + 49 optional tokens) is at scripts/theme-contract.json.

Token contract

Every theme declares the same slots: surfaces · ink · lines · primary · seal · accent · code · type · space · radius · shadow · blur · glow · motion. Components read tokens, themes set tokens, nothing else. 127 required, 49 optional.

Themes own the spacing scale. A theme declares the numbered scale --space-0 … --space-9 (contract-required), which is exactly what cia.space(4) compiles to — so a theme can ship tighter or airier rhythm without touching a component. The six t-shirt names (--space-2xs/xs/sm/md/lg/xl) are optional; the library emits them as references (--space-md: var(--space-4)), so they track the numbered scale automatically.

Component-shape tokens (--btn-radius, --card-radius, --input-radius, --modal-radius, --badge-radius, --tag-radius) are optional too, and cascade from the generic radii by default — --btn-radius: var(--radius-md, 0.25rem). Set the generic --radius-* scale to move everything; set a component token to make one thing an exception.

Icons (two systems, very different setup costs)

SVG pack — self-contained, drop-in. 49 glyphs vendored from Lucide ship at public/icons/core/<name>.svg. Nothing to install.

@use 'css-is-awesome/api' as cia;

.save-btn { @include cia.icon-svg(check); }         // tinted via currentColor
.next     { @include cia.icon-svg-text(arrow-right, $position: after); }

Adding your own glyph is genuinely drop-in — no registration step. Put star.svg in public/icons/core/ and cia.icon-svg(star) works immediately; the name resolves straight to a filename. npm run validate-icons checks the 49 contract glyphs are present and does not object to extras.

Every icon emits a per-glyph custom property, so a theme can swap one without touching SCSS:

mask: var(--cia-icon-check, url('/icons/core/check.svg')) center / contain no-repeat;
:root[data-theme='terminal'] { --cia-icon-check: url('/themes/terminal/icons/check.svg'); }

Resolution order is per-theme override → core pack → 404.

Font Awesome — bring your own fonts. cia.icon-fa(), icon-fa-icon(), icon-fa-text() and icon-fa-spin() exist for teams already on FA. They map a name through $icon-fa-map (54 entries) to a codepoint and set the FA font family:

.a { font-family: "Font Awesome 6 Free"; font-weight: 900; content: "\f00c"; }

⚠️ cia ships no Font Awesome files — FA has its own licence, so vendoring it would be wrong. The fa-* mixins compile to valid CSS but render as tofu until you:

  1. supply the .woff2 files yourself,
  2. put them where $theme-fa-path points (default /webfonts — this directory does not exist in the package),
  3. call @include cia.icon-fa-load; once at your root.

Missing font files don't error, so a silent tofu box is the failure mode. If you only want icons that work out of the box, use the SVG pack.

Recipes (build components without a component library)

cia ships no component library — deliberately. Interactive patterns arrive as recipes: portable markdown files at scss/recipes/ that give you the correct HTML, the cia.X mixin calls to style it, and an a11y checklist graded against WCAG 2.2 AA. Copy the pattern into your own framework; you own the component, cia owns the styling and the accessibility homework.

Shipped: dialog, combobox, print-to-pdf, print-spec, letterhead, mobile-nav, bottom-nav. Queued next: datepicker, data-table, command-palette.

Layout doctrine: Grid is the skeleton, Flex is the quick moves. The page shell is CSS Grid with landmark-named areas (nav / main / footer — the map reads like the page); any control-dense region inside gets its own named-area grid whose gap carries all vertical rhythm; flex lives at the leaves for one-command flips (cia.flex($direction: column)). cia.page-layout() and cia.layout() own the maps — mobile is a different area map, never margin overrides.

Mobile navigation is system API, not an exercise for the reader. Four mixins — cia.hamburger, cia.drawer, cia.sheet, cia.dock — cover both phone layouts: the flex layout (one fluid nav/main/footer shell that reshapes with the screen, hamburger opening a slide-in drawer — recipe mobile-nav) and the app layout (a fixed bottom dock whose slots open slide-up sheets — recipe bottom-nav). Zero JavaScript: everything rides CSS Grid and the native Popover API — the browser owns open state, aria-expanded, Esc and light dismiss. The docs site dogfoods the app layout on phones. The same doctrine extends to dropdowns: on phones an interactive surface takes the space it's in, so a cia.dropdown menu opens 1px under its full-width trigger at the trigger's exact width — CSS anchor positioning, flipping above when the screen bottom would clip it. The full spec is the mobile playbook.

Humans read them at /docs/recipes; AI agents pull them over MCP with list_recipes / get_recipe.

Migrating from Tailwind or Bootstrap

The cia CLI converts another design system's config into a cia theme, so you start from your existing colors and spacing instead of a blank file:

npx cia migrate tailwind ./tailwind.config.js   # auto-detects tailwind.config.* if omitted
npx cia migrate bootstrap ./scss/_variables.scss

Both accept --help for the full option list. Prose walkthroughs live at /docs/migration-tailwind and /docs/migration-bootstrap.

The CLI also carries the registry and the health check:

npx cia add --list          # browse the recipe book
npx cia fix-theme t.css     # move a theme onto current token names (--write to apply)
npx cia add bottom-nav      # copy a recipe into your project — you own the pattern
npx cia analyze src/styles  # design-system health: dead cia.* symbols, the
                            # space() scale trap, off-contract tokens (typos),
                            # off-scale lengths, hard-coded colors, BEM creep,
                            # missing focus-visible styling
npx cia theme from-tokens tokens.json --name acme --out src/styles/acme.css
                            # design tokens (DTCG v2025.10, Tokens Studio, or a
                            # flat --token map) → a complete, validated theme.css;
                            # missing required tokens inherit from a shipped base
npx cia theme map --path color.text.primary
                            # the path → token mapping from-tokens applies, as
                            # data: one path, or the whole table with --json

cia analyze reads the real API surface from the installed package and exits non-zero on errors, so it slots straight into CI. It's deliberately low-noise about color: a hex used as a var(--token, #hex) fallback is token-driven (not flagged), and a literal inside @media print is an intentional paper colour (print escapes theme colours by design). Off-contract-token findings only fire on a near-miss of a real token — a typo like --inkk — never on your own custom tokens. Off-scale-length findings suggest the nearest named step (e.g. --radius-md) for a literal border-radius/padding/margin/gap value, as a hint toward using a token — never a claim about your active theme's exact pixel value, since themes are free to set their own numbers (Terminal sets every --radius-* to 0, deliberately). The default output is a graded report — a health score, a section per concern (Contract / Spacing / Color / Naming / Layout / API / Accessibility) with a ✓ when clean, and a suggested fix on each finding; add --verbose for the flat per-file list or --json for the machine shape. Full rule reference: /docs/analyzer.

Print / PDF (zero JS)

Print support is a pure-CSS layer — the browser's native Print → Save as PDF is the generator, and cia ships no JavaScript for it:

@use 'css-is-awesome/api' as cia;

// Once, in a GLOBAL stylesheet — never inside a component module.
// It emits its own :root block plus @page, so don't wrap it in a selector.
@include cia.print-base;                   // optional flags: ($size, $margin, $freeze-animations, $link-urls, $link-origin, $page-numbers)

.site-nav   { @include cia.print-hidden; } // drop chrome on paper
.print-note { @include cia.print-only; }   // reveal paper-only content
.invoice    { @include cia.print { border: 1px solid; } } // bare @media print wrapper

print-base also collapses animations to zero duration and pins them to their final frame, so a page snapshotted mid-entrance-fade doesn't print as invisible text. It deliberately does not force opacity: 1 or transform: none — that would fix the fade while flattening every intentional use of the same properties (a 0.15 watermark, a 0.4 disabled control, a stamp rotated -4deg). Elements that were never animating are left untouched. Read --is-print (0 on screen, 1 on paper) for custom effects.

Inside @media print, print-base always forces color-scheme: light, so paired light-dark() themes print their light branch for free. It also defines a print palette — --print-ink, --print-paper, --print-line, --print-muted (default ink-on-white with light grays) — and rebinds the theme's own colour tokens (--ink, --surface-*, --border-*, --code-*) onto it, so every theme prints as clean ink-on-paper and dark-only themes (Terminal) are legible with no flag. Three opt-in flags (all default off) take it further: $link-urls prints every link's destination via attr(href); $link-origin prepends an origin so internal /… links resolve to full URLs on paper; $page-numbers numbers the sheets in the @page footer. ($legible is now a deprecated no-op — the token rebind supersedes it; passing it warns.) The docs site and the theme editor at /themes turn those flags on to print themselves as paginated spec documents.

A print theme

Those four --print-* tokens are ordinary custom properties, so print is themeable like any other surface. The default every theme inherits is clean black-on-white with light grays; a theme (or a single project) opts into its own paper identity only if it wants one — override the tokens and every element re-inks, no component touched:

// A letterhead: navy ink, silver rules. Put this in a theme's own @media print
// block, or ship it as a print-only stylesheet loaded with <link media="print">.
@media print {
  :root {
    --print-ink:   #16233f; // deep navy
    --print-muted: #5b6b86;
    --print-line:  #c7ccd6; // silver hairlines
    // --print-paper stays #fff
  }
}

Loading it as a paired print sheet is the same mechanism as paired light/dark themes:

<link rel="stylesheet" href="/themes/letterhead/print.css" media="print">

See it work. Two live demonstrations back the walkthroughs. The print-spec recipe paginates a page into a spec document (cover + table index, one page per section, honest sheet numbers). And /examples/print-to-pdf is a real invoice you Ctrl+P: on paper the site chrome and the button vanish, the invoice fills the whole sheet, its links print as full followable URLs ($link-urls + $link-origin), a sheet number appears, and a print-only QR code — generated at build time as inline SVG, zero browser JS, no image fetch — links the paper back to the live invoice-online page. Full walkthroughs: the print-to-pdf and print-spec recipes.

Why the print mixins use !important

cia avoids !important everywhere else. The print layer is the exception, and it's deliberate.

@media contributes no specificity. print-hidden is included inside your selector, so the rule it generates has exactly that selector's specificity — and any later declaration at equal specificity wins, including in print. Measured in a browser:

result in print
display: var(--print-hide) !important none ✓ hides
same rule without !important flex ✗ prints anyway

The competing display usually isn't yours — it's a utility class or a component library cia can never see. Without !important the mixin fails silently, on paper only, which is the worst place to discover it.

@layer would be worse, not better. Layered CSS always loses to unlayered CSS. If cia's print rules lived in a layer and your own CSS is unlayered (the normal case), your display: flex would win and the nav would print. cia can't require consumers to adopt layers — see .agent/decisions/decided/04-at-layer-decision.md. !important also inverts layer order, so the two don't compose the way you'd expect.

The scope is kept narrow: 8 !important declarations — seven inside @media print, and one inside @media screen (the half of print-only that hides the element on screen) — all doing one of two jobs — beating a display rule, or beating an author animation shorthand. The values stay variable-driven (--print-hide, --print-show), so you can still override behaviour without fighting the mixin.

MCP server (for AI agents)

cia ships a Model Context Protocol stdio server (JSON-RPC over stdio, protocol 2024-11-05) exposing 35 tools across 8 families (themes, mixins, functions, tokens · 127 required of them, animations, components, recipes, doc readers) plus assemble_prompt (context bundles) , resolve_size (snap design px values to cia's 4px grid) validate_theme (run the real theme validator on CSS you just wrote) theme_from_tokens (design-tokens JSON → a complete, validated theme.css) get_token_map (that mapping as data, or how one path resolves) and status (version, public-surface counts and a hash over every public name, in one ~40-token call). Any MCP-aware client (Claude Code, Cursor, Aider, Gemini, Copilot) can then query cia's real design system — mixin signatures, tokens, themes, recipes — instead of guessing, without grep-walking the repo. Full reference: /docs/mcp.

Recommended — zero install: use the dedicated css-is-awesome-mcp package. It depends on css-is-awesome and resolves your installed version's real source, so it's never out of sync — and the MCP SDK ships as a real dependency, not an optional peer you have to remember to add.

{
  "mcpServers": {
    "css-is-awesome": {
      "command": "npx",
      "args": ["css-is-awesome-mcp"]
    }
  }
}

No install command required — npx fetches and caches the package the first time your MCP client runs it. Prefer a pinned version in your own lockfile instead? npm install css-is-awesome-mcp works the same way as any other dependency; npx then runs the locally installed copy rather than fetching one:

npm install css-is-awesome-mcp

Alternative — the copy already in your node_modules: cia's own files manifest ships mcp/server.cjs too, for anyone who'd rather not add a second package. This copy needs its SDK peer deps installed manually first, since they're declared as optional peers (so a plain npm install css-is-awesome doesn't pull JS into a CSS-only install):

npm install -D @modelcontextprotocol/sdk zod
{
  "mcpServers": {
    "css-is-awesome": {
      "command": "node",
      "args": ["node_modules/css-is-awesome/mcp/server.cjs"]
    }
  }
}

Docs site

The docs site is live at https://cssisawesome.com (production — Vercel, building main on every push), with a GitHub Pages mirror at https://jerry2d3d.github.io/css-is-awesome/ that deploys after each release. To run it locally:

git clone https://github.com/Jerry2d3d/css-is-awesome.git
cd css-is-awesome
npm install
npm run dev          # http://localhost:5173

The docs site is a Next.js 16 app at src/ that dogfoods the library — every page uses CSS Modules composed from the same tokens and mixins the library ships.

It also hosts the playground: write SCSS with cia mixins, see it render live against any of the 24 themes, and share the result as a link. Sass runs in your browser (dart-sass in a web worker against cia's own source), so nothing is uploaded and the site stays a static export. Every recipe page has a “Try in playground” button.

Scripts

Script Does
npm run dev Next.js docs site on port 5173
npm run build Static-exports docs site to out/
npm run build:css Compile library SCSS to dist/css-is-awesome.css
npm run build:css:all Compile all bundles (full + core + utilities + minified) + themes + token types
npm run build:css:themes Rebuild the 24 per-theme CSS files in public/themes/ and regenerate the all-in-one public/theme.css bundle
npm run check:theme-drift Rebuild the themes into a scratch copy and fail if the committed artifacts don't match their SCSS sources
npm run check:contract Diff the token contract against the last release tag and fail if the required/optional lists changed without the version bump VERSIONING.md requires
npm run check:rtl Audit the SCSS for physical properties that should be logical (margin-left → margin-inline-start)
npm run build:token-types Generate dist/tokens.d.ts from the contract
npm run dtcg-to-scss Convert DTCG-format design tokens into cia SCSS
npm run lint ESLint on the Next.js app
npm run lint:scss Stylelint on the SCSS library
npm run validate-themes Validate every theme against the 127-required-token contract + WCAG 2.2 AA contrast (FAIL-by-default since v0.7; checks both light-dark() branches and reports the worse)
npm run validate-icons Validate the core icon pack against the 49-glyph contract
npm run validate-api Assert the css-is-awesome/api barrel stays zero-emit
npm run validate-package Pack + install into a temp project and compile every documented @use form — catches breakage that in-repo checks can't see
npm run test:surfaces Assert cia never applies a page surface automatically — the guarantee that adding hero/band tokens changes nobody's existing page
npm run test:tokens Exercise the design-tokens converter (DTCG / Tokens Studio / flat) against fixtures
npm run verify:playground Compile real samples through the playground's in-browser Sass path in Node
npm run pack:consumer Pack and install this build into a local consumer (defaults to ../boiler-project-ai); --dry-run supported
npm test Playwright suite — axe a11y checks + per-theme visual snapshots

Testing

Twelve checks, all gated in CI on every PR. Each one exists because the failure it catches actually happened.

Check What it proves
lint / lint:scss ESLint on the site, stylelint on the library
validate-themes every theme declares all 127 required contract tokens and meets WCAG 2.2 AA on 24 audited pairs — evaluating both light-dark() branches and keeping the worse result. Fails the build by default
check:theme-drift the committed public/themes/** and public/theme.css artifacts still match their SCSS sources
validate-icons the 49-glyph core pack is intact (extras allowed)
validate-api the /api barrel still emits zero CSS until a mixin is called
validate-package packs → installs into a temp project → compiles all 10 documented @use specifiers
coverage:api calls 188/188 public mixins + functions and asserts the output
coverage:mcp calls 35/35 MCP tools over stdio
validate-recipes every recipe's SCSS compiles and its documented mixin calls resolve
test Playwright — route smoke, axe a11y, per-theme visual snapshots, theme-editor behaviour, across three engines

Call-and-assert coverage. SCSS has no line-coverage tooling, so cia measures whether every part of the public API is actually callable: parse every public @mixin/@function, generate a fixture that calls it, compile, and assert it works — no null leaking into CSS, functions return a value, mixins emit. 188/188 SCSS units and 34/34 MCP tools, with CI failing below 98%. A unit with no fixture counts as uncovered, so skipping a test lowers the number rather than hiding.

What 100% means here: every public mixin and function is invoked and produces sane output. It catches renames, broken signatures and undefined variables — it found one on its first run, an undefined $icon-size that broke four icon mixins. It does not prove the CSS is visually correct; that's a deliberate trade against golden-file snapshots, which would churn dozens of files on any token change. Page-level visual correctness is covered by the Playwright snapshots instead.

Size is gated, not just claimed. npm run size-budget fails CI when any bundle outgrows its budget, and names the docs that quote the number. Raising a budget is a deliberate edit in scripts/size-budget.mjs, in the same commit as the growth — the point isn't that the number never moves, it's that it never moves silently. (These figures had drifted to be overstated by up to 2× while nothing measured them.)

Known gaps, stated plainly: a11y runs on routes, not component states — axe checks a set of pages; individual component states are not swept. (Cross-engine coverage used to be the gap here; the suite now runs chromium, firefox and webkit, which matters because cia leans on light-dark(), :has(), [popover] and mask.)

Full detail: /docs/testing.

Size (gzipped)

Bundle Size Use case
dist/tokens.css 2.25 KB Tokens only (:where(:root) CSS variables, no rules) — the purest mixin-first emit
dist/css-is-awesome.core.min.css 2.38 KB Tokens + resets, no utilities or components
dist/css-is-awesome.utilities.min.css 4.75 KB Every cia-* utility class, nothing else
dist/css-is-awesome.min.css 7.96 KB Full bundle (everything)
Per-theme themes/<name>/theme.css 2.0–3.8 KB One file per theme, both modes via light-dark(), drop-in with no markup change
Runtime JavaScript shipped in package 0 KB Nothing in the package is loaded by a page. The Node tooling (cia CLI, MCP server, validators) never reaches the browser; JS-driven UI features ship as separate add-on packages.

Status

Stable, published on npm (first published 2026-09-01). The mixin API, functions, token contract, and theme architecture are stable and under strict SemVer — breaking changes require a major bump. See VERSIONING.md for the policy.

The 1.0 surface is the v0.8 mixin-first reframe — twelve mixin renames, theme system collapsed to 8 single-file theme families, six zero-JS components, intrinsic-layout vocabulary, opt-in utilities — plus the recipes book, the Tailwind/Bootstrap migration on-ramp, print/PDF support, the in-browser Playground, the design-tokens on-ramp (npx cia theme from-tokens / theme map), and the 34-tool MCP server (now also available zero-install via the companion css-is-awesome-mcp package). The npm package ships ZERO runtime JavaScript by hard rule — nothing in it is loaded by a page; its Node tooling (CLI, MCP server, validators) never reaches the browser.

See CHANGELOG.md for the full history and MIGRATION.md for the v0.7 → v0.8 and v0.8 → v1.0 upgrade paths.

For the deep authoring reference (tier decisions, mixin contracts, agent rules), read AGENTS.md.

Launch mode / feature flags

The site ships with a runtime feature-flag gate driven by public/flags.json:

{
  "version": 1,
  "comingSoon": false,
  "comingSoonMessage": "...",
  "announcement": { "active": false, "id": "", "status": "info", "message": "", "href": "" }
}

Coming soon. Set "comingSoon": true and the site renders a full-page "we're building this" overlay on every route (the theme picker is still visible). Flip back to false to restore the normal docs experience. No rebuild required — the flag is fetched at runtime.

Announcements. Set announcement.active: true plus a unique announcement.id to show a dismissible banner on every route. Visitors can dismiss; the dismissal persists in sessionStorage keyed by id, so changing the id re-shows the banner.

Flag changes on a static host are effective after the next hard reload.

License

MIT. See LICENSE.

About

A mixin-first SCSS design system. Write your own selectors and call cia mixins: 24 themes, 32 framework-agnostic recipes, a 33-tool MCP server so AI agents read the real API, WCAG-graded themes, and zero runtime JavaScript in the package.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages