Skip to content

Chapter 61: Application 3: Design Token Editor

The third application exercises storage as the state layer and CSS as the runtime.

A design token editor lets a designer or developer edit the application’s design tokens — colors, spacing, typography, radii — and see the changes reflected immediately in a preview. The tokens persist across sessions. Changes in one tab synchronize to other open tabs. The architecture’s storage layer (Chapter 25) and CSS runtime (Chapter 27) handle most of the work.

A Design Token Editor with:

  1. A list of tokens organized by category (color, spacing, typography, radii).
  2. An edit form for the selected token.
  3. A live preview showing sample UI rendered with the current tokens.
  4. Cross-tab synchronization — changes made in one tab apply to the preview in another.
  5. Reset to defaults.
  6. Export the current token set as CSS.

The application is small architecturally but exercises specific platform capabilities cleanly.

Tokens live in localStorage keyed by their full name:

design-tokens.color.accent -> "oklch(60% 0.2 250)"
design-tokens.color.danger -> "oklch(55% 0.22 30)"
design-tokens.space.4 -> "1rem"
design-tokens.radius.md -> "0.25rem"

The flat key namespace makes the storage easy to enumerate and observe. The values are CSS strings; no transformation required to apply them.

A tokens module manages the storage:

import { defineKitModule, createProviderToken } from '@kitsune/core'
export interface TokenStore {
get(name: string): string | undefined
set(name: string, value: string): void
list(): Array<{ name: string; value: string }>
reset(): void
subscribe(handler: (event: TokenChange) => void): () => void
}
export interface TokenChange {
name: string
oldValue: string | undefined
newValue: string | undefined
source: 'local' | 'remote'
}
export const TOKENS = createProviderToken<TokenStore>('tokens')
export function tokensModule() {
return defineKitModule({
name: 'tokens',
onInstall: ({ runtime }) => {
const subscribers = new Set<(event: TokenChange) => void>()
function get(name: string) {
return localStorage.getItem(`design-tokens.${name}`) ?? undefined
}
function set(name: string, value: string) {
const key = `design-tokens.${name}`
const oldValue = localStorage.getItem(key) ?? undefined
localStorage.setItem(key, value)
notify({ name, oldValue, newValue: value, source: 'local' })
}
function list() {
const items: Array<{ name: string; value: string }> = []
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i)
if (key?.startsWith('design-tokens.')) {
items.push({
name: key.slice('design-tokens.'.length),
value: localStorage.getItem(key)!
})
}
}
return items
}
function reset() {
for (const key of Object.keys(localStorage)) {
if (key.startsWith('design-tokens.')) localStorage.removeItem(key)
}
notify({ name: '*', oldValue: undefined, newValue: undefined, source: 'local' })
}
function notify(event: TokenChange) {
for (const handler of subscribers) handler(event)
}
// Listen for cross-tab storage events
window.addEventListener('storage', (event) => {
if (!event.key?.startsWith('design-tokens.')) return
notify({
name: event.key.slice('design-tokens.'.length),
oldValue: event.oldValue ?? undefined,
newValue: event.newValue ?? undefined,
source: 'remote'
})
})
runtime.provide(TOKENS, {
get, set, list, reset,
subscribe(handler) {
subscribers.add(handler)
return () => subscribers.delete(handler)
}
})
}
})
}

The module owns the storage interaction. The subscribe method lets other modules and components react to changes (same-tab via the synthetic notification, cross-tab via the storage event).

The tokens get applied to the page through CSS custom properties on :root:

const tokenApplierModule = defineKitModule({
name: 'token-applier',
dependsOn: ['tokens'],
onStart: ({ runtime, inject }) => {
const tokens = inject(TOKENS)!
function applyAll() {
const root = document.documentElement
for (const { name, value } of tokens.list()) {
root.style.setProperty(`--${name}`, value)
}
}
applyAll()
tokens.subscribe((change) => {
if (change.name === '*') {
// Reset; re-apply all (and clear ones that no longer exist)
applyAll()
} else if (change.newValue !== undefined) {
document.documentElement.style.setProperty(`--${change.name}`, change.newValue)
} else {
document.documentElement.style.removeProperty(`--${change.name}`)
}
})
}
})

When a token changes — same tab or cross-tab — the module updates the corresponding CSS custom property on :root. Every component that uses the token (background: var(--color.accent)) immediately reflects the new value. The CSS cascade does the work.

This is the CSS as the runtime pattern (Chapter 27) applied to design tokens. No explicit re-render. No component subscription. The platform handles it.

The markup is straightforward:

<kit-shell name="token-editor">
<kit-boundary surface="token-editor" feature="design-system">
<main>
<h1>Design Tokens</h1>
<kit-boundary surface="token-list">
<h2>Tokens</h2>
<ul id="token-list"></ul>
</kit-boundary>
<kit-boundary
surface="token-form"
entity-type="token"
:entity-id="currentTokenName"
>
<h2>Edit token</h2>
<form
data-meta-event="token.save_requested"
data-meta-prop-prevent-default="true"
>
<kit-field label="Name">
<kit-text-field name="name" readonly></kit-text-field>
</kit-field>
<kit-field label="Value">
<kit-text-field name="value" required></kit-text-field>
</kit-field>
<kit-button type="submit" variant="primary">Save</kit-button>
</form>
</kit-boundary>
<kit-boundary surface="token-preview">
<h2>Preview</h2>
<div class="preview-area">
<kit-button variant="primary">Primary action</kit-button>
<kit-button variant="danger">Danger</kit-button>
<kit-card>
<h3>Sample card</h3>
<p>This card uses the current tokens for colors, spacing, and typography.</p>
</kit-card>
</div>
</kit-boundary>
</main>
</kit-boundary>
</kit-shell>

The preview region renders Kit components with their standard styling. Because the components consume the tokens through CSS custom properties, the preview reflects the current token values automatically.

When the user edits a token and saves, the orchestrator updates the storage:

const tokenOrchestrator = defineKitModule({
name: 'token-orchestrator',
dependsOn: ['tokens'],
events: {
'token.save_requested': (event) => {
const tokens = runtime.inject(TOKENS)!
const { name, value } = event.payload.data
tokens.set(name, value)
runtime.emit({
type: 'token.saved',
context: event.context,
payload: { name, value }
})
runtime.command({ type: 'notification.show', payload: { message: 'Token saved' } })
}
}
})

The flow:

  1. User edits the value field. The form’s value changes locally.
  2. User clicks Save. The form submits, firing token.save_requested.
  3. Orchestrator handles the event. Calls tokens.set(name, value).
  4. The tokens module updates localStorage and notifies subscribers.
  5. The token-applier module receives the notification. Updates the CSS custom property.
  6. Every Kit component using the token reflects the new value immediately.
  7. Orchestrator emits token.saved. Analytics tracks. Notification command dispatches. The toast appears.

The entire flow is mediated by the architecture’s primitives. The preview updates because of the CSS cascade. The cross-tab sync happens because of the platform’s storage event. The capability modules respond because of the runtime’s event distribution.

A user opens the application in two tabs. They edit a token in tab A and save. The token applier in tab A updates the CSS custom property. The platform fires a storage event in tab B (tabs of the same origin share localStorage, and modifying it in one tab notifies others). Tab B’s token applier receives the storage event and updates its CSS custom property. The preview in tab B reflects the change immediately.

No WebSockets. No polling. No application-level synchronization logic. The platform’s storage event is the architecture’s substrate; the token applier subscribes to it once and gets cross-tab consistency for free.

This is one of the architecture’s quieter wins. The same pattern works for any state that should be cross-tab consistent — user preferences, draft documents, session state, design tokens. The application doesn’t have to invent the synchronization; the platform does it.

Storage as the state layer. The tokens live in localStorage. There’s no separate in-memory store. The storage is the source of truth.

Cross-tab consistency through storage events. The platform’s storage event handles the synchronization. The application’s code subscribes once and gets the behavior.

CSS as the runtime. The token values become CSS custom properties. Components consume them through the cascade. The cascade does the propagation.

Storage subscription as the reactivity layer. The tokens module’s subscribe method lets multiple consumers react to changes without each one observing storage directly. The pattern is the architecture’s reactivity primitive.

Provider-based service exposure. The TOKENS provider is the application’s interface for token operations. The orchestrator, the applier, the UI components all inject it. The implementation can be swapped (a mock for tests, a server-synced version for multi-user editing).

Chapter 62 takes the next application — an admin table with sorting, filtering, pagination, permissions, and audit logging. The token editor’s storage-driven state extends to lists; the admin table’s per-row entity boundaries exercise the boundary system more rigorously.

Implement the token editor from this chapter.

The complete application:

  1. The tokens module that owns the storage and notifications.
  2. The token-applier module that bridges to CSS custom properties.
  3. The token-orchestrator that handles save requests.
  4. The UI markup with editor and preview regions.

Then verify:

  1. Edit a token. Watch the preview update.
  2. Open the page in a second tab. Edit a token in tab one. Watch tab two’s preview update.
  3. Close all tabs. Reopen the page. Verify the tokens persist.
  4. Use Export to dump the current tokens as CSS. Verify the output is valid.
  5. Use Reset to clear all tokens. Verify the defaults restore.

The token editor is the architecture’s storage-and-CSS pattern at its cleanest. The application is small; the platform does most of the work; the cross-tab consistency comes free.