Cameron's personal design system, packaged for use in real codebases.
v0.18.1 · 2026 — the re-true: the root is never overridden (html { font-size: 100% }), so every --t-*-size token now renders at its labeled px and the whole system scales with the browser font-size preference (#211, owner-ruled). Token-bound type grows ~14.3% — body copy lands at a true 14px (it had double-applied to 12.25px); the owner optical pass is #214. The px-literal chrome rebinds to exact-match tokens as identity swaps, the lint:tokens font-size watch arms, the .toast ghost leaves print.css, and app-shell lands on canonical .section-title. (0.18.1: the glacial whimsy now truly stills under prefers-reduced-motion — #221, a field-filed floor fix — and the uniformity doctrine is codified in § One system, many applications.) See CHANGELOG.md for what changed.
A complete, self-contained design system you can drop into any project. Five files do the heavy lifting:
| File | What it gives you |
|---|---|
src/artificer.css |
All tokens (color, type, space, radius, motion, z) + every component class. Just add this stylesheet and you have the system. |
src/artificer-theme.js |
Persistent dark/cream theme toggle, reads localStorage, respects prefers-color-scheme on first load. |
src/artificer-focus.js |
ArtificerFocus.trap(el, {onEscape}) — focus-trap helper for modals, dialogs, command palettes. |
src/artificer-icons.js |
Lucide-rooted icon set. Hydrates <i data-icon="search"> placeholders into inline SVG. |
src/tokens.json |
Machine-readable token export. For Tailwind, Figma, Style Dictionary, anything non-CSS. |
Plus src/print.css (paper-mode for PDF/print) and two SVG assets (favicon, OG card).
The optional Whimsy layer (src/artificer-whimsy.css + src/artificer-whimsy.js) is the one sanctioned exception to the no-looping-decoration / no-raw-color rules — a flowing burnished rainbow on text, for user-defined fun elements and whimsical operations only. Opt-in; load it after artificer.css. It also ships Whimsy.greeting() — a seasonal footer line (<span data-whimsy-greeting>) that lights up for Pride every June. Full doctrine in CLAUDE.md § Whimsy; interactive reference + playground at live-spec/whimsy.html.
The full live spec for every component lives at the project URL (the HTML pages — colors.html, typography.html, components.html, etc). This bundle ships the source; visit the live system for the visual reference.
Use it for — internal tools, dashboards, IDE-adjacent UI, terminals, dev/devops products, configuration UIs, productivity apps targeting power users.
Don't use it for — marketing sites, hero/landing pages, kid-facing UI, lifestyle/consumer-emotional brands. It's deliberately serious.
If you're not sure: Artificer treats the user as a peer with a job to do. If your audience is closer to "delight me" than "stop wasting my time," reach for a different system.
Copy the src/ folder into your project, then:
<!doctype html>
<html lang="en" data-theme="dark">
<head>
<link rel="stylesheet" href="/artificer/artificer.css" />
<link rel="stylesheet" href="/artificer/print.css" media="print" />
<link rel="icon" type="image/svg+xml" href="/artificer/favicon.svg" />
<script src="/artificer/artificer-theme.js" defer></script>
<script src="/artificer/artificer-icons.js" defer></script>
</head>
<body>
<a href="#main" class="skip-link">Skip to content</a>
<button class="theme-toggle" data-theme-toggle aria-label="Toggle theme">
<span class="dot"></span><span data-theme-label>Dark</span>
</button>
<main id="main" class="container container--md">…</main>
</body>
</html>That's it. Use the utility classes (.btn, .card, .stack, .cluster, etc.).
# Drop src/ into your design system folder
cp -r src/ packages/artificer/// In your app entry (e.g. src/main.ts, app/layout.tsx)
import 'artificer/artificer.css' // tokens + .tabs .appbar .crumb .sidenav .split-pane …
import 'artificer/artificer-theme.js'
import 'artificer/artificer-focus.js' // only if using modals
import 'artificer/artificer-icons.js' // only if using <i data-icon>
import 'artificer/artificer-tabs.js' // only if using .tabs (ships the APG keyboard model)
import 'artificer/artificer-options.js' // only if using .menu/.listbox/.palette (option navigation)
import 'artificer/artificer-tree.js' // only if using .tree (ships the APG tree keyboard)The CSS uses CSS custom properties (variables) and data-theme attribute — no preprocessing needed.
Nav primitives. .tabs, .appbar, .crumb, .sidenav, and .split-pane ship in artificer.css. The .tabs CSS is style-only — load artificer-tabs.js for the WAI-ARIA keyboard behavior (roving tabindex, arrows, Home/End). Full reference: live-spec/navigation.html.
<div class="tabs" data-tabs role="tablist">
<button class="tabs__tab" role="tab" aria-controls="p1">Overview</button>
<!-- … -->
</div>ArtificerTabs.observe(document) // wires APG keyboard for every [data-tabs]The CSS already provides utility classes for layout (.stack, .cluster, .grid-auto, .container). For Tailwind shops, see framework-adapters/tailwind.config.js — it imports tokens.json so your Tailwind utilities use Artificer values.
These came from research on AuDHD users; they are the system's spine. Don't refactor them away.
- Dark is default. Cream/light is a paper alternative. Never auto-switch on system theme without storing user preference.
- One primary CTA per screen. Always. If you need two equal-weight actions, you have a flow problem, not a design problem.
- Lists ≤ 7 items. Beyond that: progressive disclosure, search, or grouping. Default to 5.
- Bold anchor words in body copy — 3–5 per paragraph. They are the primary scan mechanism.
- Tier notifications by action required, not severity. A blocking error and a casual heads-up are not the same shape.
- Active state is dramatic. Inactive panes drop to 55% opacity + desaturated. Subtle differences fail the squint test.
- Tabular numbers everywhere.
font-variant-numeric: tabular-numson every numeric column. - Honor reduced-motion. Already wired — durations collapse to 0ms. Don't re-introduce hard-coded transitions.
- WCAG 2.2 AA floor, AAA on body text. Every contrast pairing is pre-measured. Don't introduce new color combos without checking.
- Pass the squint test. If a visual hierarchy disappears at squint, the contrast is wrong.
When you need to build something, find it here first instead of improvising:
| You're building… | Reach for… |
|---|---|
| A page with sidebar + main | .page-shell + .container--{sm,md,lg} |
| Stacking children vertically | .stack (with --xs/sm/md/lg/xl/2xl) |
| Horizontal toolbar / chip row | .cluster |
| Card grid | .grid-auto with --min: 240px |
| Form field | <div class="field"> with .field__label + .input + .field__hint |
| Validation error | .field--invalid + aria-invalid="true" + aria-describedby |
| Modal / dialog | .scrim + .modal, wire ArtificerFocus.trap() |
| Tooltip (label) | .tooltip |
| Popover (body content) | .popover |
| Toast / notification | .notif + tier modifier (--urgent, --attention, --info, --background) |
| Status pill | .badge + tier + .dot inside |
| Table of data | .table (add .table--zebra for dense data) |
| Headline number | Stat card pattern (see data-display.html in the live spec) |
| Empty state | .empty-state + .empty-state__title + .empty-state__body |
| Loading | Pick by duration: nothing → disabled label → .skeleton → .progress → background notification |
| Icon | <i data-icon="name" aria-hidden="true"></i> |
| Animation | var(--dur-fast) var(--ease). Don't invent durations. |
| z-index | Six rungs only: --z-{base,raised,overlay,popover,modal,toast} |
| Print/PDF output | Add <link rel="stylesheet" href="print.css" media="print"> |
For the full surface area (every component, every variant, voice & tone, accessibility checklist) read the live system or reference/SKILL.md.
The framework-adapters/ folder has minimal starting points:
tailwind.config.js— pullstokens.jsoninto Tailwind's theme. Use Artificer values viabg-bg,text-fg,text-accent,font-mono,rounded-md, etc.react-components.tsx— typed React wrappers for the most common patterns (Button, Field, Stack, Cluster, Modal, Notification). Thin — they just emit the right classes.vue-components.vue— same, for Vue 3 SFC.
These are starters, not the whole system. They cover the 80% case; for new patterns, write the markup directly and use the CSS classes.
Artificer ships with two SIL OFL typefaces, self-hosted as WOFF2 in src/assets/fonts/. Pick the body face by surface kind (see CLAUDE.md → "First decision — what surface is this?"):
- JetBrains Mono (weights 400 / 500 / 700) — body face on tool surfaces (dashboards, consoles, terminals, settings panels, data tables). Always used for code, identifiers, file paths, and numerals — even in document surfaces.
- iA Writer Quattro (weights 400 / 700, with italics) — body face on document surfaces (writeups, READMEs, reports, design docs). On tool surfaces, used for labels, hints, microcopy. Designed by Bold Monday for iA Writer; humanist sans tuned to share rhythm with monospace.
A useful rule of thumb: if the page has more than ~3 paragraphs of running prose, it's a document — set body in Quattro. If it's mostly chrome around data, it's a tool — set body in JetBrains Mono. Mixing within a project is normal: the settings page is a tool, the README explaining it is a document.
JetBrains Mono is on Google Fonts; Quattro is not — that's why we self-host. The CSS @font-face chain points at the bundled WOFF2 files and falls back through the iA Writer Quattro variants → Iowan Old Style → Charter → Source Sans 3 → system-ui, so the page still renders if fonts haven't loaded yet.
If you want the iA Writer Quattro S variant (proportional sans with tabular numerals, ideal for stacked dashboard data) or Quattro V (display variant), download them from iaolo/iA-Fonts and drop the WOFF2 into src/assets/fonts/. The CSS already prefers them.
You don't need to reinvent any of this. Just don't undo it.
- AAA contrast on all body text (12.8:1 dark, 11.4:1 cream)
- Focus-visible outlines on every interactive element (2px
--accent, 2px offset) :focus-visiblenot:focus— no rings on mouse clicksprefers-reduced-motion: reducecollapses all durations to 0msprefers-color-schemehonored on first load (overridable via toggle)- Skip-link styled, just include it on every page
.sr-onlyfor screen-reader-only text- Custom checkbox/radio/toggle keyboard-navigable, focus-ringed
- Selection color (
::selection) themed - Scrollbar styled to match (Webkit + Firefox)
The live-spec/a11y.html page (in the live system) has the 12-point shipping checklist.
<html data-theme="dark"> <!-- default; reads from localStorage -->
<html data-theme="light"> <!-- cream paper mode -->artificer-theme.js handles persistence. To toggle programmatically:
document.documentElement.setAttribute('data-theme', 'light');
localStorage.setItem('artificer.theme', 'light');To read theme preference:
const theme = document.documentElement.getAttribute('data-theme'); // 'dark' | 'light'The CSS uses color-scheme: dark / light so form controls and scrollbars match without extra work.
- Live preview pages. Open the project URL to see every component rendered, with code samples next to each.
- Logo files in raster formats. Logo is
assets/cameron-logo.jpgin the project; not included here because it's Cameron's personal mark, not a system component. - Storybook config. Add your own — Artificer is framework-agnostic.
- Tests. Add your own — components are CSS classes, test what you build with them.
Artificer also ships as themes for the surfaces where Cameron actually lives — same palette, four installs:
- Claude Code —
themes/claude-code/artificer-{dark,light}.json - Ghostty —
themes/ghostty/artificer-{dark,light} - VS Code / Cursor —
themes/vscode/(sideload orvsce package) - Obsidian —
themes/obsidian/Artificer/(drop-in theme folder, hand-authored sister)
Claude Code, Ghostty, and VS Code are generated from themes/_palette.json.
To re-tune any color across those three surfaces in lockstep:
# 1. Edit the hex in themes/_palette.json
# 2. Regenerate the three generated surfaces:
node themes/build.mjs
# 3. Manually re-check obsidian/Artificer/theme.css (--art-* tokens at top)Obsidian is hand-authored because its aesthetic is intentionally distinct (gold-as-primary instead of steel, Ghostty-grey background instead of indigo-ink, dramatic 0.55/0.6 inactive-pane recession). The palette tokens stay in sync manually.
See themes/README.md for install instructions per surface.
Maintainer-facing. Both run in CI on every PR; consumers never need either.
# Lane 1 · data gates — zero dependencies, instant
npm test # unit tests (node --test scripts/*.test.mjs)
npm run check:contrast # WCAG floors on $roles.syntax
npm run lint:palette # every CSS hex is a palette value
npm run check:version # all version stamps agree
npm run check:livespec # src/ <-> live-spec/ mirror parity
# Lane 2 · browser trust layer — Playwright (devDependencies only)
npm install # once; then:
npx playwright install chromium webkit # once; downloads engines
npm run test:browser # behavioral assertions on live-spec/
npm run test:browser:headed # watch it run
npm run test:browser:ui # interactive debuggingThe browser lane loads live-spec/ pages in Chromium (Chrome class) and
WebKit as iPhone 13 (Safari class — on iOS every browser is WebKit) and
asserts behavior: no horizontal overflow, touch-target floors, a11y, keyboard
patterns. The data gates check what the system says; the browser lane checks
what it does.
- Live system — every page is a live spec. Read first, then code.
docs/STATE.md— the maturity baseline: where each layer of the system honestly sits (clay / materials / rooms / trust), known gaps, and the backlog policy. Re-assessed each minor version.reference/SKILL.md— the AI-handoff version of these instructions, dense and machine-readable. If you're using Claude Code, point it at that file.reference/CLAUDE.md— drop into your repo root; Claude Code reads it automatically and will follow Artificer's rules from then on.
- v0.1 · 2026 · personal use, no license required for Cameron's projects. For external use: Apache-2.0 (see LICENSE).
- Any change to a token value is a minor version bump. Adding a new token or component is a patch. Removing a token is a major (breaking).
This handoff package was generated from the Cameron Personal Design System project.