
Uxspec
- 2 installs
- 5 repo stars
- Updated March 20, 2026
- johnlindquist/uxspec
Walks through building a .uxspec.json file that unifies a component's state machine and visual specification, then compiling and verifying the output.
About
An interactive workflow for creating UXSpec files, gathering requirements, designing the state machine, defining visuals, then compiling and verifying. A developer uses it when they want to scaffold a .uxspec.json describing a component's states and visuals for AI agents.
- Phased flow: requirements, state machine, visuals, compile, verify
- Every compound state needs an initial child; events map to guarded transitions
Uxspec by the numbers
- 2 all-time installs (skills.sh)
- Ranked #1,564 of 1,880 Design & UI/UX skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/johnlindquist/uxspec --skill uxspecAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| repo stars | ★ 5 |
| Last updated | March 20, 2026 |
| Repository | johnlindquist/uxspec ↗ |
What it does
Walks through building a .uxspec.json file that unifies a component's state machine and visual specification, then compiling and verifying the output.
Files
UXSpec Builder
Interactive workflow for creating UXSpec files — the JSON format that unifies state machines and visual specifications for AI agents.
Trigger
Use when the user wants to create, build, or scaffold a .uxspec.json file, or asks "help me write a uxspec".
Workflow
Follow these phases in order. Each phase produces a concrete artifact before moving on.
Phase 1 — Gather Requirements
Ask the user: 1. What is this component/page? (e.g., "login form", "toast notifications", "media player") 2. What are the key states? (e.g., "idle, loading, error, success") 3. What events drive transitions? (e.g., "SUBMIT, INPUT_CHANGED, HTTP_OK, HTTP_ERROR") 4. What context data is needed? (e.g., "email: string, password: string, error: string | null") 5. Any reusable elements? (e.g., "input field with label, action button")
Summarize back to the user as a bullet list before proceeding.
Phase 2 — Design the State Machine
Build the $machine section:
- Define compound states (parents) and leaf states (children)
- Every compound state needs an
initialchild - Map events to transitions with optional guards and actions
- Add
entry/exitactions where needed (e.g., set loading flag, clear errors) - Add
invokefor long-running effects (HTTP calls, timers)
Present the state graph as a text diagram:
idle --SUBMIT--> loading
loading --HTTP_OK--> success
loading --HTTP_ERROR--> error
error --RETRY--> loadingPhase 3 — Define Tokens and Visuals
1. Define $tokens — colors, spacing, radius, font, timing (follow W3C Design Tokens format) 2. Define $elements — reusable element templates with params 3. Add $visual to each leaf state — container layout, slots with elements 4. Add testId to interactive elements for assertions 5. Add aria labels for accessibility
Read references/format-reference.md for token syntax, element types, and visual structure.
Phase 4 — Add Runtime Semantics
1. Define $context — typed fields with defaults 2. Define $events — with source and payload types 3. Define $actions — assign, emit, log 4. Define $effects — http, timer, navigate, focus 5. Wire binding, visibleWhen, enabledWhen, onPress, onChange on elements
Read references/runtime-semantics.md for expression syntax and action/effect kinds.
Phase 5 — Generate and Verify
1. Write the spec to examples/<name>.uxspec.json 2. Validate: bun run src/compiler/cli.ts validate examples/<name>.uxspec.json 3. If validation fails, fix reported issues (the compiler returns structured issue codes) 4. Compile: bun run src/compiler/cli.ts compile examples/<name>.uxspec.json 5. Inspect: bun run src/compiler/cli.ts inspect examples/<name>.uxspec.json 6. Report to the user:
- Number of states and assertions
- Whether initial resolves to a leaf state
- Any unresolved refs or token aliases
- The compiled output path
Issue Resolution
If the compiler reports issues, map codes to fixes:
| Code | Fix |
|---|---|
UNDECLARED_CONTEXT_VAR | Add missing field to $context |
UNDECLARED_EVENT | Add missing event to $events |
UNDECLARED_TARGET | Fix transition target to match a valid state path |
UNSUPPORTED_EXPR_OP | Replace with a supported operator (see spec) |
INVALID_ASSIGN_PATH | Ensure path starts with context. |
UNKNOWN_TOKEN_REFERENCE | Add missing token to $tokens or fix the path |
UNKNOWN_ELEMENT_REFERENCE | Add missing element to $elements or fix the name |
INVALID_MACHINE_INITIAL | Set $machine.initial to a valid top-level state |
INVALID_COMPOUND_INITIAL | Set compound state initial to a valid child name |
Key Rules
- File extension:
.uxspec.json - Schema:
https://uxspec.dev/0.2/schema.json - Required top-level:
$schema,$description,$machine $machinerequires:id,initial,states- Leaf elements (
text,button,input,icon,shape,badge,bar) MUST NOT havechildren - Container elements (
group,layer,grid,stack-h,stack-v) MAY havechildren - Token references:
"{path.to.token}"— curly braces inside a string - Element references:
{ "$ref": "name", ...params } - Expressions: S-expression arrays like
["==", ["var", "context.x"], true] - All
$-prefixed properties are reserved
Reference Files
references/format-reference.md— Token syntax, element types, visual structure, layout primitivesreferences/runtime-semantics.md— Context, events, actions, effects, expressions, transitionsreferences/minimal-example.md— Complete minimal spec you can use as a starting template
UXSpec Format Reference
Token Syntax (W3C Design Tokens)
Tokens use $type and $value. Reference with "{path.to.token}".
"$tokens": {
"color": {
"primary": { "$type": "color", "$value": "#0070f3" },
"error": { "$type": "color", "$value": "#ee5555" }
},
"spacing": {
"sm": { "$type": "dimension", "$value": { "value": 8, "unit": "px" } },
"md": { "$type": "dimension", "$value": { "value": 16, "unit": "px" } }
},
"radius": {
"md": { "$type": "dimension", "$value": { "value": 8, "unit": "px" } }
},
"font": {
"family": { "$type": "fontFamily", "$value": ["Inter", "system-ui", "sans-serif"] },
"size": {
"base": { "$type": "dimension", "$value": { "value": 14, "unit": "px" } }
},
"weight": {
"medium": { "$type": "number", "$value": 500 }
}
},
"timing": {
"normal": { "$type": "duration", "$value": { "value": 250, "unit": "ms" } }
}
}Element Types
Containers (MAY have children)
group— generic layout containerlayer— Z-axis stacking (ZStack)grid— grid layoutstack-h— horizontal stack (HStack / Row)stack-v— vertical stack (VStack / Column)
Leaves (MUST NOT have children)
text— styled text, usecontentfor the stringinput— text input, usebinding.inputTypefor typebutton— interactive, useonPressfor actionsicon— named icon withsizeandcolorshape— primitive shape (circle, rectangle)badge— small status indicatorbar— data-driven bar
Reusable Elements ($elements)
"$elements": {
"form-input": {
"type": "group",
"layout": "stack-v",
"gap": "{spacing.xs}",
"params": ["label", "name", "inputType"],
"children": [
{ "type": "text", "content": { "$bind": "label" }, "fontSize": "{font.size.sm}" },
{ "type": "input", "name": { "$bind": "name" }, "binding": { "inputType": { "$bind": "inputType" } } }
]
}
}Reference with: { "$ref": "form-input", "label": "Email", "name": "email", "inputType": "email" }
Visual Structure ($visual)
"$visual": {
"$description": "What this state looks like",
"container": {
"layout": "stack-v",
"padding": "{spacing.lg}",
"gap": "{spacing.md}",
"background": "{color.surface}",
"borderRadius": "{radius.md}"
},
"slots": {
"header": [
{ "type": "text", "content": "Title", "fontSize": "{font.size.lg}" }
],
"body": [
{ "type": "input", "name": "email", "testId": "email-input" },
{ "type": "button", "name": "submit", "content": "Submit", "testId": "submit-btn" }
]
},
"onEnter": { "focus": "email", "animation": "page-enter" },
"keyboard": { "Enter": "SUBMIT" }
}Animations ($animations)
"$animations": {
"fade-in": {
"$description": "Fade in on entry",
"duration": "{timing.normal}",
"easing": "ease-out",
"keyframes": {
"0": { "opacity": 0 },
"100": { "opacity": 1 }
}
}
}Interaction States
"interactions": {
"hover": { "background": "{color.accentHover}" },
"focus": { "outlineWidth": { "value": 2, "unit": "px" }, "outlineColor": "rgba(0, 112, 243, 0.3)" },
"disabled": { "opacity": 0.5 }
}Slot Inheritance
Child states inherit parent $visual slots. A child only needs to declare slots that change. If a child defines slots, it fully replaces the parent's slots.
Minimal UXSpec Example
A complete, minimal spec for a simple counter component. Use as a starting template.
{
"$schema": "https://uxspec.dev/0.2/schema.json",
"$description": "Simple counter — increment and decrement buttons with a display. Minimal example demonstrating core UXSpec patterns.",
"$tokens": {
"color": {
"bg": { "$type": "color", "$value": "#111111" },
"surface": { "$type": "color", "$value": "#1a1a1a" },
"text": { "$type": "color", "$value": "#ffffff" },
"accent": { "$type": "color", "$value": "#0070f3" }
},
"spacing": {
"sm": { "$type": "dimension", "$value": { "value": 8, "unit": "px" } },
"md": { "$type": "dimension", "$value": { "value": 16, "unit": "px" } }
},
"radius": {
"md": { "$type": "dimension", "$value": { "value": 8, "unit": "px" } }
},
"font": {
"size": {
"base": { "$type": "dimension", "$value": { "value": 14, "unit": "px" } },
"xl": { "$type": "dimension", "$value": { "value": 24, "unit": "px" } }
}
}
},
"$context": {
"count": { "type": "number", "default": 0, "$description": "Current counter value" }
},
"$events": {
"INCREMENT": { "source": "user", "payload": {} },
"DECREMENT": { "source": "user", "payload": {} },
"RESET": { "source": "user", "payload": {} }
},
"$machine": {
"id": "counter",
"initial": "active",
"states": {
"active": {
"on": {
"INCREMENT": {
"target": "active",
"actions": [{ "kind": "assign", "path": "context.count", "value": ["+", ["var", "context.count"], 1] }]
},
"DECREMENT": {
"target": "active",
"actions": [{ "kind": "assign", "path": "context.count", "value": ["-", ["var", "context.count"], 1] }]
},
"RESET": {
"target": "active",
"actions": [{ "kind": "assign", "path": "context.count", "value": 0 }]
}
},
"$visual": {
"$description": "Counter display with increment, decrement, and reset buttons",
"container": {
"layout": "stack-v",
"padding": "{spacing.md}",
"gap": "{spacing.md}",
"background": "{color.surface}",
"borderRadius": "{radius.md}",
"alignItems": "center"
},
"slots": {
"display": [
{
"type": "text",
"name": "count-display",
"binding": { "content": ["var", "context.count"] },
"fontSize": "{font.size.xl}",
"color": "{color.text}",
"testId": "count-display"
}
],
"controls": [
{
"type": "group",
"layout": "stack-h",
"gap": "{spacing.sm}",
"children": [
{
"type": "button",
"name": "decrement",
"content": "-",
"onPress": [{ "kind": "emit", "event": "DECREMENT" }],
"background": "{color.accent}",
"testId": "decrement-btn"
},
{
"type": "button",
"name": "reset",
"content": "Reset",
"onPress": [{ "kind": "emit", "event": "RESET" }],
"testId": "reset-btn"
},
{
"type": "button",
"name": "increment",
"content": "+",
"onPress": [{ "kind": "emit", "event": "INCREMENT" }],
"background": "{color.accent}",
"testId": "increment-btn"
}
]
}
]
}
}
}
}
}
}Compile and Verify
bun run src/compiler/cli.ts validate examples/counter.uxspec.json
bun run src/compiler/cli.ts compile examples/counter.uxspec.json
bun run src/compiler/cli.ts inspect examples/counter.uxspec.jsonExpected output:
ok: true- 1 state, 4 assertions (count-display, decrement-btn, reset-btn, increment-btn)
leafInitial: true- 0 unresolved refs, 0 unresolved token aliases
Runtime Semantics Reference
Context ($context)
Typed runtime state. Every field needs type and default.
"$context": {
"email": { "type": "string", "default": "", "$description": "Email input value" },
"submitting": { "type": "boolean", "default": false },
"error": { "type": "string", "default": null },
"items": { "type": "array", "default": [] }
}Allowed types: string, number, boolean, object, array, null.
Events ($events)
"$events": {
"SUBMIT": { "source": "user", "payload": {} },
"INPUT_CHANGED": { "source": "user", "payload": { "name": { "type": "string" }, "value": { "type": "string" } } },
"HTTP_OK": { "source": "network", "payload": { "data": { "type": "object" } } },
"HTTP_ERROR": { "source": "network", "payload": { "message": { "type": "string" } } },
"TICK": { "source": "timer", "payload": {} }
}Sources: user, system, timer, network, storage.
Actions ($actions) — Pure State Mutations
| Kind | Required | Example |
|---|---|---|
assign | path, value | { "kind": "assign", "path": "context.email", "value": ["var", "event.value"] } |
emit | event | { "kind": "emit", "event": "SUBMIT" } |
log | level, message | { "kind": "log", "level": "info", "message": "submitted" } |
assign path MUST start with context.. emit event MUST be declared in $events.
Effects ($effects) — Side Effects
| Kind | Required | Example |
|---|---|---|
http | request | { "kind": "http", "request": "signInRequest" } |
timer.start | id, ms, event | { "kind": "timer.start", "id": "cooldown", "ms": 3000, "event": "COOLDOWN_EXPIRED" } |
timer.cancel | id | { "kind": "timer.cancel", "id": "cooldown" } |
navigate | to | { "kind": "navigate", "to": "/dashboard" } |
focus | target | { "kind": "focus", "target": "email" } |
storage.write | key, value | { "kind": "storage.write", "key": "token", "value": ["var", "context.token"] } |
Transitions
Short form: "on": { "SUBMIT": "loading" }
Object form with guard and actions:
"on": {
"SUBMIT": {
"target": "loading",
"guard": ["==", ["var", "context.submitting"], false],
"actions": [{ "kind": "assign", "path": "context.submitting", "value": true }]
}
}always transitions (evaluated on state entry, MUST have guard):
"always": [
{ "target": "success", "guard": ["==", ["var", "context.verified"], true] }
]Entry, Exit, Invoke
"loading": {
"entry": [
{ "kind": "assign", "path": "context.loading", "value": true },
{ "kind": "http", "request": "fetchData" }
],
"exit": [
{ "kind": "assign", "path": "context.loading", "value": false }
],
"invoke": [
{ "kind": "timer.start", "id": "poll", "ms": 5000, "event": "POLL_TICK" }
]
}Expression Syntax (S-expressions)
Arithmetic: +, -, *, /, pow
Comparison: ==, !=, <, <=, >, >=
Boolean: !, &&, ||
Access: var, get, coalesce
Control: if
Math: min, max, clamp, lerp, round, floor, ceilExamples:
["var", "context.email"]— read context value["==", ["var", "context.error"], null]— equality check["&&", ["!=", ["var", "context.email"], ""], ["==", ["var", "context.submitting"], false]]— compound guard["if", ["var", "context.loading"], "Loading...", "Submit"]— conditional text
Element Runtime Properties
| Property | Type | Description |
|---|---|---|
binding | object | Maps element props to expressions: { "content": ["var", "context.label"] } |
visibleWhen | expr | Show only when truthy |
enabledWhen | expr | Interactive only when truthy |
onPress | array | Actions/effects on click |
onChange | array | Actions/effects on value change |
testId | string | Stable test identifier (becomes data-testid) |
aria | object | Accessibility: { "label": "Submit form", "live": "polite" } |