A token-driven Svelte design system built on shadcn-svelte, with two independent design axes instead of one.
Most systems give you light/dark — a color switch. This one adds a second, orthogonal
character axis: soft ↔ hard. Soft is rounded corners, diffuse shadows, and a
sans-serif voice; hard is sharp corners, hard offset shadows, thick contrast borders, and
a serif voice. Flip either axis on <html> and every component re-skins — no per-component
code, because all customization flows through design tokens.
data-mode = soft | hard ← character (radius, shadow, font, border, motion)
data-theme = light | dark | pink… ← color (surface, fg, accent, border, danger)
data-theme is an open, named set — light/dark/pink ship today and more can be
added. A <Button> only ever references semantic tokens (var(--radius), var(--accent)),
so every mode × theme combination falls out of attribute changes alone.
- Two-axis token system — soft/hard character × an open named color theme set, fully orthogonal.
- The full shadcn-svelte suite, conformed to the tokens via a single
@themebridge — components inherit both axes with zero edits. - Layout primitives (
Stack,Cluster,Grid,Container) + a token-drivenText. - Live editors —
/playground(tune the character tokens) and/theme-builder(edit any theme or draft/delete new named ones), both exporting committable CSS. - Distributable — installable as
@wl/frontend-system, pinned and delivered via a Nix flake; consumers theme entirely through tokens.
nix develop # pinned Node 22 (or use your own Node 18+)
npm install
npm run dev # → http://localhost:5173| Route | What it is |
|---|---|
/showcase |
The full component catalog under one active mode; toggle character/color at the top |
/playground |
Live-edit the soft/hard character tokens; export modes/*.css |
/theme-builder |
Edit any theme's color roles, or draft/delete new named themes; export themes/<name>.css |
Common scripts:
npm run check # svelte-check (type + a11y)
npm run package # build the distributable library (svelte-package + publint) → dist/A consuming Svelte/TS project onboards in one command:
nix flake init -t github:WilsonLessley14/frontend-system#consumer
nix develop # sets $FRONTEND_SYSTEM_TGZ (the pinned tarball)
npm install && npm install "$FRONTEND_SYSTEM_TGZ"
npm run devThe setup contract the template wires for you:
/* app.css — Tailwind first, then the design system (it ships @source so Tailwind
scans its components for class usage) */
@import 'tailwindcss';
@import '@wl/frontend-system/styles.css';<!-- app.html -->
<html data-mode="soft" data-theme="light"><script>
import { Button, Card, Stack } from '@wl/frontend-system';
</script>Peer deps the consumer needs: svelte 5, tailwindcss 4, bits-ui, tailwind-variants,
clsx, tailwind-merge, @lucide/svelte, tw-animate-css.
Everything is pinned in the consumer's flake.lock; bump with
nix flake update frontend-system then reinstall the tarball.
After changing a component or token, publish a new version:
- Bump the version in
package.json(and the matchingversionfield inflake.nix). - Sync the lockfile —
npm install(a version bump always rewritespackage-lock.json). - Recompute the deps hash — because the lockfile changed, the flake's pinned
npmDepsHashis now stale. Get the new one and paste it intoflake.nix:(Skip this only ifnix run nixpkgs#prefetch-npm-deps -- package-lock.json
package-lock.jsonis unchanged — e.g. a component-only edit with no version bump. A stale hash makesnix buildfail with the correct hash, so it's self-correcting either way.) - Verify —
nix build .#frontend-system(runssvelte-package+publint). - Commit & push —
git commit -am "release: vX.Y.Z" && git push origin main.
Consumers then pull it with nix flake update frontend-system + reinstall (see above).
Nice shortcut. Steps 1–4 are mechanical (and the hash recompute is easy to forget), so:
npm run release -- patch # or: minor | major | 1.2.3bumps the version, syncs flake.nix, recomputes npmDepsHash, and verifies the build —
then prints the git commit / git push to run. The publish itself stays in your hands.
Don't edit components — customize through tokens:
- Switch axes — set
data-mode/data-themeon<html>. - Retune values — build a mode in
/playgroundor a theme in/theme-builder, export the CSS, and@importit after the package stylesheet so its[data-mode=…]/[data-theme=…]rules win.
Tokens layer in three tiers: primitives (raw scales) → semantic (the role contract
components consume) → axis files (modes/*, themes/*) that remap the semantic roles.
src/
lib/ # the published library (@wl/frontend-system)
index.ts # public API barrel
styles.css # tokens + @theme bridge + @source (consumer stylesheet)
utils.ts # cn() + element-ref types
components/ui/ # the conformed shadcn-svelte suite
design/
tokens/ # primitives, semantic, modes/*, themes/*
components/layout/ # Stack, Cluster, Grid, Container
config/ # typed token + axis manifests (drive the editors)
editor-format.ts # pure token → CSS serialization
routes/ # the dev app (showcase, playground, theme-builder)
_dev/ # dev-only helpers (kept out of the package)
flake.nix # devShell, package tarball, consumer template
templates/consumer/ # the `nix flake init` template
DESIGN_SYSTEM.md # full architecture & decision record
SvelteKit · Svelte 5 (runes) · Tailwind v4 · shadcn-svelte · Bits UI · Nix flakes.
See DESIGN_SYSTEM.md for the architecture, the conform bridge, the editor contracts, distribution details, and the full decision record.