@motion-proto/live-tokens
v0.40.1
Published
Design token editor with live CSS variable editing. Svelte 5 + Vite 8.
Maintainers
Readme
Live Tokens
A foundational design system for quickly styling and building Svelte + Vite microsites. Edit your tokens and components in real time — colors, typography, spacing, per-component aliases — and see the site update as you drag the slider. Save the result as a portable configuration you can carry from project to project.
npm install @motion-proto/live-tokens into your app — install once, style fast. The editor is dev-only; production builds get plain CSS variables and your chosen components, nothing else.
What you get
- Real-time token editing. Pick a color, drag a hue slider, retype a font size — the page repaints on every input event via CSS-variable writes. No reload, no save-and-refresh, no build step. Works across colors, typography, spacing, radii, shadows, motion, palettes, and gradients.
- Real-time component editing. Each of ~24 shipped Svelte components (Button, Input, Card, Dialog, Badge, Callout, Table, Tooltip, Toggle, TabBar, SegmentedControl, RadioButton, MenuSelect, ProgressBar, CornerBadge, SectionDivider, CollapsibleSection, Notification, Image, ImageLightbox, CodeSnippet, SideNavigation, and more) declares its own design-token aliases in a
:global(:root)block. Rewire any alias from a per-component picker and see that component update everywhere it's used — live, on your real pages, not in a Storybook sandbox. - Theme editor (
/live-tokens/editorroute, dev-only) — the home of real-time token editing. Save themes to disk as JSON, promote one to "production" to bake it into a statictokens.cssfor the build. - Per-component editor (
/live-tokens/componentsroute, dev-only) — the home of real-time component-alias editing. Pick token aliases per component without writing CSS. - Live editor overlay — pins to the top-right of every dev page. Opens the editor in a side panel or floating window so you edit on the page you're styling, not in a separate tab. Includes a "Page Source" button that opens the current page's
.sveltefile in VS Code. - Manifests — a manifest captures a whole site configuration as one portable artifact: the theme in one slot, every component in its own slot, each holding either the shipped default or a custom file. Export it as a bundle and import it into another project to restore the full styling in one step.
- Vite plugin — hosts the
/api/live-tokens/{themes,component-configs,manifests}/*routes that persist your edits to disk as you make them. The single namespace keeps live-tokens' routes from colliding with anything your app serves under/api. - Claude Code skill suite — three bundled skills so you can drive the package in plain English.
build-pagecomposes pages from the shipped components.pick-componentdecides between confusing pairs (TabBar vs SegmentedControl, Card vs CollapsibleSection).create-componentauthors a new editable component against the project's naming, state-model, and import rules. One command to install all three:npx @motion-proto/live-tokens setup-claude. See Claude Code skills below.
Quick install
npm install @motion-proto/live-tokensVite config
// vite.config.ts
import { defineConfig } from 'vite';
import { svelte, vitePreprocess } from '@sveltejs/vite-plugin-svelte';
import { themeFileApi } from '@motion-proto/live-tokens/vite-plugin';
export default defineConfig({
plugins: [
// vitePreprocess compiles the shipped components' `<style lang="scss">`
// blocks — install `sass` alongside it.
svelte({ preprocess: vitePreprocess() }),
themeFileApi({
tokensCssPath: 'src/system/styles/tokens.css',
}),
],
});The themeFileApi plugin:
- Seeds
src/live-tokens/data/themes/with a default theme on first dev-server start. - Discovers components at
src/components/*.svelte(andsrc/system/components/*.sveltefor back-compat) and seedssrc/live-tokens/data/component-configs/{comp}/default.jsonfrom each component's:global(:root)block. - Hosts the
/api/live-tokens/*routes the editor uses to save and load themes + per-component configs. - Auto-injects
__PROJECT_ROOT__for the overlay's "Page Source" link and__LIVE_TOKENS_API_BASE__so the client uses whateverapiBaseyou configured.
Where data lands — and how to move it
By default, the plugin reads and writes under one folder: src/live-tokens/data/. Inside that folder live three subdirectories — themes/, manifests/, component-configs/ — each owned by the plugin.
To move them, create a live-tokens.config.json at your project root:
{
"dataDir": "src/live-tokens/data"
}All four keys are optional. dataDir is the headline knob — it relocates all three subfolders at once. The per-folder overrides exist for unusual layouts (e.g. a monorepo where themes are shared across packages but component-configs aren't):
{
"dataDir": "src/live-tokens/data",
"themesDir": "../shared/themes",
"componentConfigsDir": "src/live-tokens/data/component-configs",
"manifestsDir": "src/live-tokens/data/manifests"
}Resolution order, per folder: explicit themeFileApi(opts) argument > matching key in live-tokens.config.json > <dataDir>/<sub>. The dev server reads the file once at startup — restart vite to pick up changes.
Bootstrap in main.ts
// main.ts
import '@motion-proto/live-tokens/app/tokens.css';
import './live-tokens/data/tokens.generated.css';
import '@motion-proto/live-tokens/app/fonts.css';
import { bootLiveTokens } from '@motion-proto/live-tokens';
import App from './App.svelte';
bootLiveTokens(App, '#app');bootLiveTokens orchestrates the editor's idempotent init hooks, fetches the
active theme in dev, optionally registers consumer-authored components, and
mounts the app. FontAwesome is side-effect-imported by the bootstrap (the dev
overlay always needs icons). The three token-CSS imports stay with the
consumer because order matters and tokens.generated.css is project-local.
Pass components to register consumer-authored editable components:
import MyWidgetEditor, { allTokens as myWidgetTokens } from './components/MyWidgetEditor.svelte';
bootLiveTokens(App, '#app', {
components: [{
id: 'mywidget',
label: 'My Widget',
icon: 'fas fa-magic',
sourceFile: 'src/components/MyWidget.svelte',
editorComponent: MyWidgetEditor,
schema: myWidgetTokens,
}],
});Mount routes with <LiveTokensRouter>
<!-- App.svelte -->
<script lang="ts">
import { LiveTokensRouter } from '@motion-proto/live-tokens';
</script>
<LiveTokensRouter pages={{
'/': { lazy: () => import('./Home.svelte'), label: 'Home', icon: 'fa-home', source: 'src/Home.svelte' },
}} /><LiveTokensRouter> owns the dev overlay (<LiveEditorOverlay> +
<ColumnsOverlay>), the editor routes (/live-tokens/editor, /live-tokens/components, /live-tokens/docs), the
in-app link-click interception, and the nav-rail/page-source plumbing the
overlay needs. Each entry in pages is one of your routes; entries with a
label appear in the overlay's nav rail. Pass pages as lazy: () => import('./Page.svelte')
so each page's stylesheet side-effects only evaluate when that route is
visited; pass component: PageComponent instead for an eagerly-imported
page. The editor routes are dispatched internally, so you don't have to
dynamic-import the library's editor pages yourself.
For routes you can't enumerate ahead of time (a /:id or /:slug, a path
prefix, or a page shown only when some condition holds), add a resolve
function from the current path to a RouteEntry; return null to fall
through. It's plain code, so params, prefixes, and gating are a regex and an
if, and the package ships no route syntax of its own. Resolution order is
pages[path], then resolve(path), then the pages['/'] fallback, so adding
resolve never changes how existing pages entries match. A resolved entry
can carry props, letting one page component serve many paths (such as the
matched id), and its source gives the dynamic route a working "Page Source"
button just like a static one.
<LiveTokensRouter
pages={{ '/': { lazy: () => import('./Home.svelte'), label: 'Home' } }}
resolve={(path) => {
const m = path.match(/^\/module\/(.+)$/);
if (!m) return null;
return { lazy: () => import('./ModulePage.svelte'), props: { id: m[1] }, source: 'src/ModulePage.svelte' };
}}
/>You can also relocate or disable a default editor route via the
editorRoutes prop: <LiveTokensRouter pages={…} editorRoutes={{ editor: '/admin/editor', components: false }} />.
Pass a string to move a route; pass false to remove the route entirely
(no dispatch and, for components, no auto-injected nav-rail entry).
The whole overlay surface is dev-only and tree-shakes out of production
builds — no {#if import.meta.env.DEV} guards needed.
Lower-level API (when you need it)
bootLiveTokens and <LiveTokensRouter> are convenience wrappers. The
individual init functions (initCssVarSync, initRouter,
initColumnsOverlay, initEditorStore, initializeTheme),
<LiveEditorOverlay>, <ColumnsOverlay>, and the editor page exports
(@motion-proto/live-tokens/editor,
@motion-proto/live-tokens/component-editor-page) all stay exported. Use
them directly to build a custom shell: render arbitrary markup per route, host
a foreign matcher, or drive the overlay yourself. You do not need this for
dynamic or gated routes; reach for resolve above, which keeps the overlay,
nav rail, and page-source intact.
Use components
<script lang="ts">
import Button from '@motion-proto/live-tokens/components/Button.svelte';
import Callout from '@motion-proto/live-tokens/components/Callout.svelte';
</script>
<Callout variant="info">Read this.</Callout>
<Button variant="primary">Save</Button>The components carry their own design-token aliases (declared inside each .svelte file). They'll pick up your tokens.css overrides automatically. Strip out the ones you don't use; nothing is forced.
Styles
Editor chrome (ui-editor.css, ui-form-controls.css) and the icon font are
auto-loaded by the editor pages themselves; you don't import them. The
only stylesheet a consumer needs is a tokens.css declaring the design-token
CSS variables on :root.
You can use the package's default as a starting point:
import '@motion-proto/live-tokens/app/tokens.css';
import '@motion-proto/live-tokens/app/site.css'; // optional: themed h1/p/a styles
import '@motion-proto/live-tokens/app/fonts.css'; // optional: Fraunces + Manrope @font-face…or copy node_modules/@motion-proto/live-tokens/src/system/styles/tokens.css into
your project and edit. The editor will seed themes/default.json on first
run and you can promote your edits back into the file.
Consuming live-tokens from scratch
The minimum a consumer needs after npm install @motion-proto/live-tokens:
// src/main.ts
import '@motion-proto/live-tokens/app/tokens.css';
import { mount } from 'svelte';
import App from './App.svelte';
mount(App, { target: document.getElementById('app')! });<!-- src/App.svelte -->
<script lang="ts">
import Editor from '@motion-proto/live-tokens/editor';
</script>
<Editor />// vite.config.ts
import { defineConfig } from 'vite';
import { svelte, vitePreprocess } from '@sveltejs/vite-plugin-svelte';
export default defineConfig({
plugins: [svelte({ preprocess: vitePreprocess() })],
});No css: 'injected' workaround, no optimizeDeps excludes — vite build works as-is. (You'll want the full themeFileApi plugin and bootLiveTokens / <LiveTokensRouter> from the Quick install section above when you're ready to persist edits to disk and ship a real app.)
Greenfield? Scaffold a new app
If you're starting from scratch, skip the manual wiring:
npx @motion-proto/live-tokens create my-app
cd my-app
npm install
npm run devThis generates a Svelte + Vite app that depends on the package — vite.config.ts, main.ts, App.svelte, the themeFileApi plugin, and a placeholder src/pages/Home.svelte are all pre-wired. The token CSS is seeded from the version you scaffolded against, so it never drifts. Open http://localhost:5173 and replace Home.svelte with your content; upgrade the package later with npm update.
(The older npx degit motionproto/live-tokens route cloned this whole repo as your app — the package's source, tests, and all. create gives you a thin consumer app instead.)
Recommended project layout
create scaffolds the preferred integration surface. Whether you scaffolded or wired up by hand, conforming to this layout keeps upgrades non-destructive and consistent across projects.
src/
main.ts # token CSS chain → bootLiveTokens(App, '#app')
App.svelte # routes (e.g. <LiveTokensRouter {pages} />)
pages/ # your pages
styles/site.css # your themed page typography (yours to edit)
system/styles/tokens.css # vendored Layer-1 tokens — committed
live-tokens/data/ # editor state — committed
tokens.generated.css # editor output
themes/ manifests/ component-configs/
**/_backups/ # gitignored (local-only snapshots)
vite.config.ts # svelte({ preprocess: vitePreprocess() }) + themeFileApi
svelte.config.js # vitePreprocess()Conventions that make this work:
- Vendor
tokens.cssintosrc/and commit it. PointthemeFileApi({ tokensCssPath })at that file, not at one insidenode_modules. The dev server writes your edits there; a copy undernode_modulesis wiped on everynpm install. - All editable state lives under
src/and is committed —tokens.css,tokens.generated.css, and everything inlive-tokens/data/. This is the invariant that makes upgrades safe:npm installonly ever touchesnode_modules+package.json+ the lockfile, never yoursrc/. _backups/is gitignored. The dev server snapshots a file before overwriting it; those snapshots are local working state, not source.- Preprocess with
vitePreprocess()(bundled in@sveltejs/vite-plugin-svelte), keepingsassinstalled for the components'scss. Nosvelte-preprocess, nolegacy-peer-deps.npmrc— the dependency tree resolves cleanly on its own (since 0.19.1). - Import only from the public surface —
@motion-proto/live-tokens,/components/*,/vite-plugin,/app/*.
Consumer-authored components
The shipped components are first-party by default, but you can author your own and get the same real-time editing experience. Co-locate runtime and editor files in src/components/ (or src/system/components/, both are scanned by default) and pass them to bootLiveTokens:
// src/main.ts
import { bootLiveTokens } from '@motion-proto/live-tokens';
import App from './App.svelte';
import MyWidgetEditor, { allTokens as myWidgetTokens } from './components/MyWidgetEditor.svelte';
bootLiveTokens(App, '#app', {
components: [{
id: 'mywidget',
label: 'My Widget',
icon: 'fas fa-magic',
sourceFile: 'src/components/MyWidget.svelte',
editorComponent: MyWidgetEditor,
schema: myWidgetTokens,
}],
});(bootLiveTokens calls registerComponent internally for each entry, gated on import.meta.env.DEV so the registration tree-shakes out of production builds. Call registerComponent directly if you need finer control over timing.)
The component appears in the /live-tokens/components page under a CUSTOM group in the nav rail. Token rows, linked-block sharing, per-component config persistence, and reset-to-default work identically to the built-in set. All imports must come from @motion-proto/live-tokens or @motion-proto/live-tokens/component-editor; never deep-import from src/.
Claude Code skills
The package ships a suite of Claude Code skills that encode the project's conventions so Claude can drive the package in plain English. They cover the three jobs the README itself can't carry well: deciding which shipped component fits a need, composing a page from the catalogue, and (for the long-tail case) authoring a new editable component. Each skill auto-triggers from natural-language requests — no slash commands. (Plain npm install plus the README handle first-time setup.)
| Skill | Triggers on | What it knows |
|--------------------------------|----------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------|
| live-tokens-build-page | "build a pricing page using live-tokens components" | shipped-component catalogue, column grid, pageSources registration, token-only styling rule |
| live-tokens-pick-component | "what's the difference between TabBar and SegmentedControl?" | decision tables for each confusable family (selection, container, messaging, on/off); when to author a new one instead |
| live-tokens-create-component | "author a new Toggle component for my live-tokens project" | runtime + editor + registerComponent() recipe, naming scheme, state model, public-imports rule, verification checklist |
Install
npx @motion-proto/live-tokens setup-claudeCopies every bundled skill into ./.claude/skills/ in the current directory. Re-run after upgrading the package to pick up new or updated skills (pass --force to overwrite). macOS/Linux only.
If you'd rather avoid the CLI, the equivalent one-liner:
mkdir -p .claude/skills && cp -R node_modules/@motion-proto/live-tokens/.claude/skills/. .claude/skills/Validate authored components
The same CLI ships a static validator that turns the create-component skill's verification checklist into a runnable command:
npx @motion-proto/live-tokens check-component <id>It enforces the file layout, :global(:root) block, token-suffix vocabulary, the state-before-property rule, the no-raw-colour-defaults rule, the public-imports rule, and the registerComponent({ id }) call. Exit code 0 means the static contract is met. Useful both as a self-check after Claude generates a component and as a pre-commit guard on human-authored ones.
How the editor ships changes to prod
- Edit in
/live-tokens/editoror/live-tokens/components. Saves write to<dataDir>/themes/{name}.jsonand<dataDir>/component-configs/{comp}/{name}.json. - Promote a theme to "production." Its variables are written into
tokens.generated.cssnext to your authoredtokens.css. npm run buildbundles both as plain CSS. No editor code, no JSON lookups, no dev surfaces ship to prod.
File ownership — what the plugin writes
Knowing which files the plugin touches matters when upgrading the package or working in a repo you don't want overwritten. For a plain-language version of how your saved look stays safe across upgrades while tokens.css holds the building blocks, see TOKENS.md.
On npm install or npm update: nothing outside node_modules/. No install hooks. Upgrading versions never touches your src/live-tokens/data/, or any file in src/ outside it.
The plugin only writes inside two locations on disk:
src/live-tokens/data/(configurable vialive-tokens.config.json— see "Where data lands").- The CSS sidecars next to your
tokensCssPath(tokens.generated.css,fonts.css).
It never writes to your project root, your src/ outside the data folder, or anywhere else.
At dev-server startup, the plugin only fills gaps — it never overwrites authored files:
<dataDir>/themes/default.json— written only if missing.<dataDir>/themes/_active.jsonand_production.json— written only if missing.<dataDir>/component-configs/{comp}/_active.jsonand_production.json— same: only if missing.<dataDir>/component-configs/{comp}/default.json— regenerated from the component's:global(:root)block only when the.sveltesource is newer than the existing default. This file is a build artifact of the source; don't hand-edit it.
At dev-time editor actions, these files get rewritten by your explicit save/promote:
<dataDir>/themes/{name}.json— every save in the editor.<tokensCssPath sibling>/tokens.generated.css— fully regenerated when you save or promote the production theme.<tokensCssPath sibling>/fonts.css— same rule: regenerated from the theme's font sources.<dataDir>/component-configs/{comp}/{name}.json— every save of a per-component config.
The developer-authored tokens.css itself is never written by the plugin — it holds defaults you're free to hand-edit. The editor's overrides land in the sidecar tokens.generated.css, which the package imports immediately after tokens.css.
The one exception is the opt-in themeFileApi({ autoMigrate: true }) option. When enabled, the dev server applies pending additive token migrations (new token names only) to your tokens.css at startup and writes the file, so it stays current with the package as you upgrade. The change shows up in git for review. Breaking migrations (rename/remove) are never auto-applied; run npx live-tokens migrate for those during a deliberate upgrade. Off by default, so the "never written" rule holds unless you turn it on. See TOKENS.md.
License
MIT. Originally extracted from RuneGoblin.
