Language Specification for AI
Complete Constela language specification for AI-assisted code generation
How to Use This Page
Copy the entire Markdown block below and paste it to your AI assistant (Claude, ChatGPT, etc.). The AI will then be able to generate valid Constela code.
Tip
Select all text inside the code block below, copy it, and paste it into your AI conversation.
markdown
# Constela Language Specification
Constela is a JSON-based declarative UI language. This specification provides everything you need to generate valid Constela code.
## Key Principles
1. ALL values must be wrapped in expressions
2. Event handlers use a specific object structure
3. State is typed and immutable (updates create new values)
---
## Program Structure
Every Constela program has this root structure:
```json
{
"version": "1.0",
"state": { ... },
"actions": [ ... ],
"view": { ... },
"components": { ... },
"styles": { ... },
"imports": { ... },
"connections": { ... },
"lifecycle": { ... },
"route": { ... },
"data": { ... },
"externalImports": { ... },
"theme": { ... }
}
```
| Property | Required | Description |
|----------|----------|-------------|
| `version` | Yes | Always `"1.0"` |
| `state` | Yes | State declarations object |
| `actions` | Yes | Array of action definitions |
| `view` | Yes | Root view node |
| `components` | No | Reusable component definitions |
| `styles` | No | CVA-style presets |
| `imports` | No | Static JSON file imports |
| `connections` | No | WebSocket connections |
| `lifecycle` | No | Mount/unmount hooks |
| `route` | No | Routing configuration |
| `data` | No | Build-time data sources (including AI generation) |
| `externalImports` | No | External JS module URLs |
| `theme` | No | Theme configuration (colors, fonts, mode) |
---
## State Declarations
State fields are declared with type and initial value:
```json
{
"state": {
"count": { "type": "number", "initial": 0 },
"name": { "type": "string", "initial": "" },
"isOpen": { "type": "boolean", "initial": false },
"items": { "type": "list", "initial": [] },
"user": { "type": "object", "initial": { "name": "", "email": "" } }
}
}
```
| Type | Initial Value Examples |
|------|----------------------|
| `number` | `0`, `100`, `-1` |
| `string` | `""`, `"hello"` |
| `boolean` | `true`, `false` |
| `list` | `[]`, `[1, 2, 3]` |
| `object` | `{}`, `{ "key": "value" }` |
---
## Expressions (22 types)
All dynamic values in Constela use expressions.
### Expression Quick Reference
| Expression | Syntax | Use Case |
|------------|--------|----------|
| `lit` | `{ "expr": "lit", "value": X }` | Static/literal values |
| `state` | `{ "expr": "state", "name": "N" }` | Access state fields |
| `var` | `{ "expr": "var", "name": "N", "path": "P" }` | Loop variables, event data |
| `param` | `{ "expr": "param", "name": "N", "path": "P" }` | Action parameters |
| `bin` | `{ "expr": "bin", "op": "O", "left": L, "right": R }` | Binary operations |
| `not` | `{ "expr": "not", "operand": E }` | Boolean negation |
| `cond` | `{ "expr": "cond", "if": C, "then": T, "else": E }` | Conditional value |
| `get` | `{ "expr": "get", "base": B, "path": "P" }` | Property access |
| `route` | `{ "expr": "route", "name": "N", "source": "param\|query\|path" }` | Route parameters |
| `index` | `{ "expr": "index", "base": B, "key": K }` | Dynamic array/object access |
| `import` | `{ "expr": "import", "name": "N", "path": "P" }` | Imported JSON data |
| `cookie` | `{ "expr": "cookie", "key": "K", "default": "D" }` | Cookie values |
| `data` | `{ "expr": "data", "name": "N", "path": "P" }` | Build-time data sources |
| `ref` | `{ "expr": "ref", "name": "N" }` | DOM element references |
| `style` | `{ "expr": "style", "name": "N", "variants": V }` | Style presets |
| `concat` | `{ "expr": "concat", "items": [...] }` | String concatenation |
| `validity` | `{ "expr": "validity", "ref": "R", "property": "P" }` | Form validation |
| `call` | `{ "expr": "call", "target": T, "method": "M", "args": A }` | Method calls |
| `lambda` | `{ "expr": "lambda", "param": "P", "body": B }` | Anonymous functions |
| `array` | `{ "expr": "array", "elements": [...] }` | Dynamic arrays |
| `local` | `{ "expr": "local", "name": "N" }` | Component local state values |
| `obj` | `{ "expr": "obj", "props": { "key": expr } }` | Object literal construction |
### Binary Operators
`+`, `-`, `*`, `/`, `%`, `==`, `!=`, `<`, `<=`, `>`, `>=`, `&&`, `||`
### Expression Examples
```json
// Literal value
{ "expr": "lit", "value": "Hello" }
{ "expr": "lit", "value": 42 }
{ "expr": "lit", "value": true }
// State access
{ "expr": "state", "name": "count" }
// Variable (loop item, event data)
{ "expr": "var", "name": "item" }
{ "expr": "var", "name": "item", "path": "title" }
{ "expr": "var", "name": "event", "path": "target.value" }
// Action parameter (inside action steps)
{ "expr": "param", "name": "event", "path": "target.value" }
// Binary operation
{
"expr": "bin",
"op": "+",
"left": { "expr": "state", "name": "count" },
"right": { "expr": "lit", "value": 1 }
}
// Boolean negation
{ "expr": "not", "operand": { "expr": "state", "name": "isLoading" } }
// Conditional value (NOT for rendering - use if node for that)
{
"expr": "cond",
"if": { "expr": "state", "name": "isAdmin" },
"then": { "expr": "lit", "value": "Admin" },
"else": { "expr": "lit", "value": "User" }
}
// Property access
{
"expr": "get",
"base": { "expr": "state", "name": "user" },
"path": "profile.name"
}
// String concatenation
{
"expr": "concat",
"items": [
{ "expr": "lit", "value": "Hello, " },
{ "expr": "state", "name": "name" },
{ "expr": "lit", "value": "!" }
]
}
// Method call
{
"expr": "call",
"target": { "expr": "state", "name": "items" },
"method": "filter",
"args": [{
"expr": "lambda",
"param": "item",
"body": { "expr": "get", "base": { "expr": "var", "name": "item" }, "path": "active" }
}]
}
// Local state access (in components with localState)
{ "expr": "local", "name": "isExpanded" }
// Object literal construction
{
"expr": "obj",
"props": {
"x": { "expr": "var", "name": "item", "path": "x" },
"y": { "expr": "var", "name": "item", "path": "y" }
}
}
```
---
## Theme System
Configure application theming with CSS variables:
```json
{
"theme": {
"mode": "system",
"colors": {
"primary": "hsl(220 90% 56%)",
"primary-foreground": "hsl(0 0% 100%)",
"background": "hsl(0 0% 100%)",
"foreground": "hsl(222 47% 11%)",
"muted": "hsl(210 40% 96%)",
"border": "hsl(214 32% 91%)"
},
"darkColors": {
"background": "hsl(222 47% 11%)",
"foreground": "hsl(210 40% 98%)"
},
"fonts": {
"sans": "Inter, system-ui, sans-serif",
"mono": "JetBrains Mono, monospace"
},
"cssPrefix": "app"
}
}
```
| Property | Type | Description |
|----------|------|-------------|
| `mode` | `'light' \| 'dark' \| 'system'` | Color scheme mode |
| `colors` | `ThemeColors` | Light mode color tokens |
| `darkColors` | `ThemeColors` | Dark mode color overrides |
| `fonts` | `ThemeFonts` | Font family definitions |
| `cssPrefix` | `string` | CSS variable prefix (e.g., `--app-primary`) |
---
## View Nodes (12 types)
### View Node Quick Reference
| Node | Syntax | Purpose |
|------|--------|---------|
| `element` | `{ "kind": "element", "tag": "T", "props": P, "children": C }` | HTML elements |
| `text` | `{ "kind": "text", "value": EXPR }` | Text content |
| `if` | `{ "kind": "if", "condition": C, "then": T, "else": E }` | Conditional rendering |
| `each` | `{ "kind": "each", "items": I, "as": "A", "index": "I", "key": K, "body": B }` | List iteration |
| `component` | `{ "kind": "component", "name": "N", "props": P, "children": C }` | Reusable components |
| `slot` | `{ "kind": "slot" }` | Children placeholder |
| `markdown` | `{ "kind": "markdown", "content": EXPR }` | Markdown rendering |
| `code` | `{ "kind": "code", "language": EXPR, "content": EXPR }` | Syntax highlighting |
| `portal` | `{ "kind": "portal", "target": "T", "children": C }` | Render outside tree |
| `island` | `{ "kind": "island", "id": "I", "strategy": "S", "content": C }` | Partial hydration |
| `suspense` | `{ "kind": "suspense", "id": "I", "fallback": F, "content": C }` | Async content |
| `errorBoundary` | `{ "kind": "errorBoundary", "fallback": F, "content": C }` | Error handling |
### View Node Examples
```json
// Element with props and children
{
"kind": "element",
"tag": "button",
"props": {
"className": { "expr": "lit", "value": "btn" },
"disabled": { "expr": "state", "name": "isLoading" },
"onClick": { "event": "click", "action": "handleClick" }
},
"children": [
{ "kind": "text", "value": { "expr": "lit", "value": "Click me" } }
]
}
// Text node (value MUST be an expression)
{ "kind": "text", "value": { "expr": "lit", "value": "Hello" } }
{ "kind": "text", "value": { "expr": "state", "name": "message" } }
// Conditional rendering
{
"kind": "if",
"condition": { "expr": "state", "name": "isLoggedIn" },
"then": { "kind": "text", "value": { "expr": "lit", "value": "Welcome!" } },
"else": { "kind": "text", "value": { "expr": "lit", "value": "Please log in" } }
}
// List iteration
{
"kind": "each",
"items": { "expr": "state", "name": "todos" },
"as": "todo",
"index": "i",
"key": { "expr": "var", "name": "todo", "path": "id" },
"body": {
"kind": "element",
"tag": "li",
"children": [
{ "kind": "text", "value": { "expr": "var", "name": "todo", "path": "title" } }
]
}
}
// Component usage
{
"kind": "component",
"name": "Button",
"props": {
"label": { "expr": "lit", "value": "Submit" }
}
}
// Portal (render to body)
{
"kind": "portal",
"target": "body",
"children": [
{ "kind": "element", "tag": "div", "props": { "className": { "expr": "lit", "value": "modal" } }, "children": [...] }
]
}
// Island (partial hydration)
{
"kind": "island",
"id": "interactive-counter",
"strategy": "visible",
"strategyOptions": { "threshold": 0.5, "rootMargin": "100px" },
"content": { "kind": "component", "name": "Counter" },
"state": { "count": { "type": "number", "initial": 0 } },
"actions": [{ "name": "increment", "steps": [...] }]
}
// Suspense (async content with fallback)
{
"kind": "suspense",
"id": "user-data",
"fallback": { "kind": "text", "value": { "expr": "lit", "value": "Loading..." } },
"content": { "kind": "component", "name": "UserProfile" }
}
// ErrorBoundary (error handling with fallback)
{
"kind": "errorBoundary",
"fallback": { "kind": "text", "value": { "expr": "lit", "value": "Something went wrong" } },
"content": { "kind": "component", "name": "RiskyComponent" }
}
```
### Island Hydration Strategies
| Strategy | Description | Options |
|----------|-------------|---------|
| `load` | Hydrate immediately on page load | - |
| `idle` | Hydrate when browser is idle | `timeout` (ms) |
| `visible` | Hydrate when element enters viewport | `threshold` (0-1), `rootMargin` |
| `interaction` | Hydrate on first user interaction | - |
| `media` | Hydrate when media query matches | `media` (query string) |
| `never` | Never hydrate (static only) | - |
---
## Transition Directive
`if` and `each` nodes support an optional `transition` property for CSS class-based enter/exit animations.
### Transition Properties
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `enter` | `string` | No | CSS class applied on the first frame of enter |
| `enterActive` | `string` | No | CSS class applied during the enter transition |
| `exit` | `string` | No | CSS class applied on the first frame of exit |
| `exitActive` | `string` | No | CSS class applied during the exit transition |
| `duration` | `number` | No | Transition duration in ms (must be non-negative) |
### Example
```json
{
"kind": "if",
"condition": { "expr": "state", "name": "showMessage" },
"then": {
"kind": "element",
"tag": "div",
"props": { "className": { "expr": "lit", "value": "toast" } },
"children": [
{ "kind": "text", "value": { "expr": "lit", "value": "Saved successfully!" } }
]
},
"transition": {
"enter": "fade-enter",
"enterActive": "fade-enter-active",
"exit": "fade-exit",
"exitActive": "fade-exit-active",
"duration": 300
}
}
```
### CSS for the transition
```css
.fade-enter { opacity: 0; }
.fade-enter-active { transition: opacity 300ms ease; opacity: 1; }
.fade-exit { opacity: 1; }
.fade-exit-active { transition: opacity 300ms ease; opacity: 0; }
```
**Enter flow:** Element created → `enter` class → next frame: swap to `enterActive` → transitionend: cleanup.
**Exit flow:** `exit` class → next frame: swap to `exitActive` → transitionend: remove from DOM.
> **SSR Note:** Transitions are client-side only. Server rendering ignores transition directives.
---
## Action Steps (27 types)
Actions define responses to events. Each action has a name and array of steps.
### Action Step Quick Reference
| Step | Syntax | Purpose |
|------|--------|---------|
| `set` | `{ "do": "set", "target": "T", "value": V }` | Set state value |
| `update` | `{ "do": "update", "target": "T", "operation": "O", "value": V }` | Update state with operation |
| `fetch` | `{ "do": "fetch", "url": U, "method": "M", "body": B, "result": "R", "onSuccess": [], "onError": [] }` | HTTP request |
| `navigate` | `{ "do": "navigate", "url": U, "target": "_self\|_blank" }` | Page navigation |
| `storage` | `{ "do": "storage", "operation": "get\|set\|remove", "key": K, "storage": "local\|session" }` | Browser storage |
| `dom` | `{ "do": "dom", "operation": "addClass\|removeClass\|toggleClass\|setAttribute", "selector": S }` | DOM manipulation |
| `import` | `{ "do": "import", "module": "M", "result": "R", "onSuccess": [] }` | Dynamic JS import |
| `call` | `{ "do": "call", "target": T, "args": A, "result": "R" }` | Call JS function |
| `subscribe` | `{ "do": "subscribe", "target": T, "event": "E", "action": "A" }` | Event subscription |
| `dispose` | `{ "do": "dispose", "target": T }` | Cleanup resources |
| `clipboard` | `{ "do": "clipboard", "operation": "read\|write", "value": V, "result": "R" }` | Clipboard access |
| `if` | `{ "do": "if", "condition": C, "then": [], "else": [] }` | Conditional steps |
| `setPath` | `{ "do": "setPath", "target": "T", "path": P, "value": V }` | Update nested value |
| `send` | `{ "do": "send", "connection": "C", "data": D }` | WebSocket send |
| `close` | `{ "do": "close", "connection": "C" }` | WebSocket close |
| `delay` | `{ "do": "delay", "ms": M, "then": [] }` | Delayed execution |
| `interval` | `{ "do": "interval", "ms": M, "action": "A", "result": "R" }` | Repeated execution |
| `clearTimer` | `{ "do": "clearTimer", "target": T }` | Stop timer |
| `focus` | `{ "do": "focus", "target": T, "operation": "focus\|blur\|select" }` | Focus control |
| `generate` | `{ "do": "generate", "provider": "P", "prompt": EXPR, "output": "O", "result": "R", "onSuccess": [], "onError": [] }` | AI DSL generation |
| `sseConnect` | `{ "do": "sseConnect", "connection": "C", "url": U, "reconnect": R, "onMessage": [] }` | SSE connection |
| `sseClose` | `{ "do": "sseClose", "connection": "C" }` | Close SSE connection |
| `optimistic` | `{ "do": "optimistic", "target": "T", "path": P, "value": V, "result": "R" }` | Optimistic UI update |
| `confirm` | `{ "do": "confirm", "id": I }` | Confirm optimistic update |
| `reject` | `{ "do": "reject", "id": I }` | Reject/rollback optimistic update |
| `bind` | `{ "do": "bind", "connection": "C", "target": "T", "transform": X }` | Bind SSE to state |
| `unbind` | `{ "do": "unbind", "connection": "C", "target": "T" }` | Unbind SSE from state |
### Update Operations by State Type
| State Type | Operations |
|------------|------------|
| `number` | `increment`, `decrement` |
| `boolean` | `toggle` |
| `list` | `push`, `pop`, `remove`, `replaceAt`, `insertAt`, `splice` |
| `object` | `merge` |
### Action Examples
```json
{
"actions": [
{
"name": "increment",
"steps": [
{ "do": "update", "target": "count", "operation": "increment" }
]
},
{
"name": "updateInput",
"steps": [
{
"do": "set",
"target": "inputValue",
"value": { "expr": "param", "name": "event", "path": "target.value" }
}
]
},
{
"name": "addTodo",
"steps": [
{
"do": "update",
"target": "todos",
"operation": "push",
"value": { "expr": "state", "name": "newTodo" }
},
{
"do": "set",
"target": "newTodo",
"value": { "expr": "lit", "value": "" }
}
]
},
{
"name": "toggleTodo",
"steps": [
{
"do": "setPath",
"target": "todos",
"path": {
"expr": "array",
"elements": [
{ "expr": "param", "name": "index" },
{ "expr": "lit", "value": "completed" }
]
},
"value": {
"expr": "not",
"operand": { "expr": "param", "name": "completed" }
}
}
]
},
{
"name": "fetchData",
"steps": [
{ "do": "set", "target": "isLoading", "value": { "expr": "lit", "value": true } },
{
"do": "fetch",
"url": { "expr": "lit", "value": "https://api.example.com/data" },
"method": "GET",
"result": "data",
"onSuccess": [
{ "do": "set", "target": "isLoading", "value": { "expr": "lit", "value": false } }
],
"onError": [
{ "do": "set", "target": "error", "value": { "expr": "param", "name": "error" } }
]
}
]
}
]
}
```
---
## Event Handling
Event handlers in element props use this structure:
```json
{
"props": {
"onClick": { "event": "click", "action": "handleClick" },
"onInput": { "event": "input", "action": "handleInput" },
"onSubmit": { "event": "submit", "action": "handleSubmit" },
"onKeyDown": { "event": "keydown", "action": "handleKeyDown" }
}
}
```
### With Payload
Pass additional data to the action:
```json
{
"onClick": {
"event": "click",
"action": "deleteItem",
"payload": {
"id": { "expr": "var", "name": "item", "path": "id" },
"index": { "expr": "var", "name": "i" }
}
}
}
```
Access payload in action via `param`:
```json
{
"name": "deleteItem",
"steps": [
{
"do": "update",
"target": "items",
"operation": "splice",
"index": { "expr": "param", "name": "index" },
"deleteCount": { "expr": "lit", "value": 1 }
}
]
}
```
---
## Components
Define reusable components:
```json
{
"components": {
"Button": {
"params": {
"label": { "type": "string" },
"variant": { "type": "string", "default": "primary" }
},
"view": {
"kind": "element",
"tag": "button",
"props": {
"className": { "expr": "param", "name": "variant" }
},
"children": [
{ "kind": "text", "value": { "expr": "param", "name": "label" } },
{ "kind": "slot" }
]
}
}
}
}
```
Use components:
```json
{
"kind": "component",
"name": "Button",
"props": {
"label": { "expr": "lit", "value": "Submit" },
"variant": { "expr": "lit", "value": "secondary" }
},
"children": [
{ "kind": "text", "value": { "expr": "lit", "value": " →" } }
]
}
```
### Component Local State
Components can have instance-scoped state using `localState` and `localActions`:
```json
{
"components": {
"Accordion": {
"params": { "title": { "type": "string" } },
"localState": {
"isExpanded": { "type": "boolean", "initial": false }
},
"localActions": [
{
"name": "toggle",
"steps": [{ "do": "update", "target": "isExpanded", "operation": "toggle" }]
}
],
"view": {
"kind": "element",
"tag": "div",
"children": [
{
"kind": "element",
"tag": "button",
"props": { "onClick": { "event": "click", "action": "toggle" } },
"children": [
{ "kind": "text", "value": { "expr": "param", "name": "title" } }
]
},
{
"kind": "if",
"condition": { "expr": "state", "name": "isExpanded" },
"then": { "kind": "slot" }
}
]
}
}
}
}
```
| Property | Description |
|----------|-------------|
| `localState` | State declarations scoped to component instance |
| `localActions` | Actions that operate on local state |
**Rules:**
- Each component instance has independent local state
- Local actions can only use `set`, `update`, `setPath` steps (no `fetch`, `navigate`, `storage`)
- `state` expressions inside component check local state first, then fall back to global
- Use cases: accordions, dropdowns, form fields, toggles, tooltips
---
## Styles (CVA Pattern)
```json
{
"styles": {
"button": {
"base": "px-4 py-2 rounded font-medium",
"variants": {
"variant": {
"primary": "bg-blue-500 text-white",
"secondary": "bg-gray-200 text-gray-800"
},
"size": {
"sm": "text-sm h-8",
"md": "text-base h-10",
"lg": "text-lg h-12"
}
},
"defaultVariants": {
"variant": "primary",
"size": "md"
}
}
}
}
```
Use with style expression:
```json
{
"className": {
"expr": "style",
"name": "button",
"variants": {
"variant": { "expr": "lit", "value": "secondary" },
"size": { "expr": "lit", "value": "lg" }
}
}
}
```
---
## Lifecycle Hooks
```json
{
"lifecycle": {
"onMount": "initializeApp",
"onUnmount": "cleanup"
}
}
```
---
## WebSocket Connections
```json
{
"connections": {
"chat": {
"url": { "expr": "lit", "value": "wss://api.example.com/ws" },
"onMessage": "handleMessage",
"onOpen": "onConnected",
"onClose": "onDisconnected"
}
}
}
```
Send: `{ "do": "send", "connection": "chat", "data": { "expr": "state", "name": "message" } }`
Close: `{ "do": "close", "connection": "chat" }`
---
## Realtime Features (SSE)
### SSE Connection
```json
{
"do": "sseConnect",
"connection": "notifications",
"url": { "expr": "lit", "value": "/api/events" },
"eventTypes": ["message", "update"],
"reconnect": {
"enabled": true,
"strategy": "exponential",
"maxRetries": 5,
"baseDelay": 1000,
"maxDelay": 30000
},
"onOpen": [{ "do": "set", "target": "connected", "value": { "expr": "lit", "value": true } }],
"onMessage": [{ "do": "update", "target": "messages", "operation": "push", "value": { "expr": "var", "name": "payload" } }],
"onError": [{ "do": "set", "target": "error", "value": { "expr": "var", "name": "payload" } }]
}
```
| Property | Description |
|----------|-------------|
| `connection` | Connection identifier |
| `url` | SSE endpoint URL |
| `eventTypes` | Event types to listen for |
| `reconnect.strategy` | `exponential`, `linear`, or `none` |
| `onOpen/onMessage/onError` | Handler steps |
Close: `{ "do": "sseClose", "connection": "notifications" }`
### Optimistic Updates
Apply UI changes immediately with automatic rollback on failure:
```json
{
"name": "likePost",
"steps": [
{
"do": "optimistic",
"target": "posts",
"path": { "expr": "var", "name": "index" },
"value": { "expr": "lit", "value": { "liked": true } },
"result": "updateId",
"timeout": 5000
},
{
"do": "fetch",
"url": { "expr": "lit", "value": "/api/like" },
"method": "POST",
"onSuccess": [{ "do": "confirm", "id": { "expr": "var", "name": "updateId" } }],
"onError": [{ "do": "reject", "id": { "expr": "var", "name": "updateId" } }]
}
]
}
```
### State Binding
Bind SSE messages directly to state:
```json
{
"do": "bind",
"connection": "notifications",
"eventType": "update",
"target": "messages",
"transform": { "expr": "get", "base": { "expr": "var", "name": "payload" }, "path": "data" }
}
```
Unbind: `{ "do": "unbind", "connection": "notifications", "target": "messages" }`
---
## AI Integration (@constela/ai)
### AI Data Source (Build-time)
Generate content at build time using AI:
```json
{
"data": {
"hero": {
"type": "ai",
"provider": "anthropic",
"prompt": "Create a hero section with gradient background and CTA button",
"output": "component"
}
},
"view": {
"kind": "component",
"name": "hero",
"props": {}
}
}
```
| Property | Description |
|----------|-------------|
| `type` | Must be `"ai"` |
| `provider` | `"anthropic"` or `"openai"` |
| `prompt` | Natural language description |
| `output` | `"component"` or `"view"` |
### Generate Action (Runtime)
Generate DSL dynamically in response to user actions:
```json
{
"name": "generateCard",
"steps": [
{
"do": "generate",
"provider": "anthropic",
"prompt": { "expr": "state", "name": "userPrompt" },
"output": "component",
"result": "generatedCard",
"onSuccess": [
{ "do": "set", "target": "card", "value": { "expr": "var", "name": "generatedCard" } }
],
"onError": [
{ "do": "set", "target": "error", "value": { "expr": "param", "name": "error" } }
]
}
]
}
```
| Property | Description |
|----------|-------------|
| `do` | Must be `"generate"` |
| `provider` | `"anthropic"` or `"openai"` |
| `prompt` | Expression evaluating to prompt string |
| `output` | `"component"` or `"view"` |
| `result` | Variable name to store generated DSL |
| `onSuccess` | Steps to run on success |
| `onError` | Steps to run on error |
### Security Rules
AI-generated DSL is automatically validated:
**Forbidden Tags** (always blocked): `script`, `iframe`, `object`, `embed`, `form`
**Forbidden Actions** (cannot be whitelisted): `import`, `call`, `dom`
**Restricted Actions** (require explicit whitelist): `fetch`
---
## Plugin System
Extend Constela with custom global functions via plugins.
### Configuration
Add plugins to `constela.config.json`:
```json
{
"plugins": ["constela-plugin-charts", "./src/plugins/my-plugin"]
}
```
### Defining a Plugin
```typescript
import type { ConstelaPlugin } from '@constela/core';
const myPlugin: ConstelaPlugin = {
name: 'my-plugin',
version: '1.0.0',
globalFunctions: {
greet: (name: string) => `Hello, ${name}!`,
formatPrice: (amount: number) => `$${amount.toFixed(2)}`,
},
};
export default myPlugin;
```
### Using Plugin Functions in DSL
```json
{
"expr": "call",
"target": { "expr": "var", "name": "globalFunctions" },
"method": "formatPrice",
"args": [{ "expr": "state", "name": "price" }]
}
```
### Registration API
| Function | Description |
|----------|-------------|
| `registerPlugin(plugin)` | Register plugin and all its global functions (atomic) |
| `clearPlugins()` | Remove all plugins and their functions |
| `getRegisteredPlugins()` | List registered plugin names |
| `registerGlobalFunction(name, fn)` | Register a single global function |
| `unregisterGlobalFunction(name)` | Remove a single global function |
> **Security:** Names containing `__proto__`, `constructor`, or `prototype` are rejected.
---
## Accessibility (a11y) Best Practices
Constela includes compile-time accessibility validation. The compiler checks 7 rules and reports warnings (compilation is not blocked).
### a11y Rules
| Code | Rule | Fix |
|------|------|-----|
| `IMG_NO_ALT` | `<img>` missing `alt` | Add `alt` prop |
| `BUTTON_NO_LABEL` | `<button>` has no label | Add text child or `aria-label` |
| `ANCHOR_NO_LABEL` | `<a>` has no label | Add text child or `aria-label` |
| `INPUT_NO_LABEL` | `<input>` has no label | Add `aria-label` prop |
| `HEADING_SKIP` | Heading levels skipped | Use sequential headings (h1 → h2 → h3) |
| `POSITIVE_TABINDEX` | Positive `tabindex` | Use `tabindex="0"` or `-1` instead |
| `DUPLICATE_ID` | Duplicate `id` values | Use unique `id` per element |
### Accessible Form Example
```json
{
"kind": "element",
"tag": "form",
"children": [
{
"kind": "element",
"tag": "h1",
"children": [{ "kind": "text", "value": { "expr": "lit", "value": "Contact Us" } }]
},
{
"kind": "element",
"tag": "h2",
"children": [{ "kind": "text", "value": { "expr": "lit", "value": "Your Information" } }]
},
{
"kind": "element",
"tag": "img",
"props": {
"src": { "expr": "lit", "value": "/logo.png" },
"alt": { "expr": "lit", "value": "Company logo" }
}
},
{
"kind": "element",
"tag": "input",
"props": {
"type": { "expr": "lit", "value": "email" },
"aria-label": { "expr": "lit", "value": "Email address" }
}
},
{
"kind": "element",
"tag": "button",
"children": [{ "kind": "text", "value": { "expr": "lit", "value": "Submit" } }]
}
]
}
```
---
## Common Pitfalls
### WRONG vs CORRECT
| WRONG | CORRECT | Rule |
|-------|---------|------|
| `"value": "text"` | `"value": { "expr": "lit", "value": "text" }` | All values must be expressions |
| `"onClick": "handleClick"` | `"onClick": { "event": "click", "action": "handleClick" }` | Event handlers are objects |
| `"onClick": { "action": "handleClick" }` | `"onClick": { "event": "click", "action": "handleClick" }` | Must include `event` property |
| `{ "expr": "state", "name": "item" }` in each | `{ "expr": "var", "name": "item" }` | Loop variables use `var` |
| `{ "kind": "text", "value": "Hello" }` | `{ "kind": "text", "value": { "expr": "lit", "value": "Hello" } }` | Text value must be expression |
| `{ "do": "update", "target": "count", "operation": "toggle" }` | Use `toggle` only for boolean | Match operation to state type |
### if Node vs cond Expression
| Purpose | Use |
|---------|-----|
| Conditionally **render** elements | `if` node |
| Compute a **value** conditionally | `cond` expression |
### state vs var vs param
| Context | Expression |
|---------|------------|
| Top-level application state | `state` |
| Component local state | `state` (checks local first, then global) |
| Inside `each` loop (item/index) | `var` |
| Inside action steps (event data) | `param` with `name: "event"` |
| Inside action steps (fetch result) | `param` with `name: "result"` |
| Component params (in component view) | `param` |
---
## Complete Example: Counter
```json
{
"version": "1.0",
"state": {
"count": { "type": "number", "initial": 0 }
},
"actions": [
{
"name": "increment",
"steps": [{ "do": "update", "target": "count", "operation": "increment" }]
},
{
"name": "decrement",
"steps": [{ "do": "update", "target": "count", "operation": "decrement" }]
}
],
"view": {
"kind": "element",
"tag": "div",
"children": [
{
"kind": "element",
"tag": "button",
"props": { "onClick": { "event": "click", "action": "decrement" } },
"children": [{ "kind": "text", "value": { "expr": "lit", "value": "-" } }]
},
{ "kind": "text", "value": { "expr": "state", "name": "count" } },
{
"kind": "element",
"tag": "button",
"props": { "onClick": { "event": "click", "action": "increment" } },
"children": [{ "kind": "text", "value": { "expr": "lit", "value": "+" } }]
}
]
}
}
```
## Complete Example: Todo List
```json
{
"version": "1.0",
"state": {
"todos": { "type": "list", "initial": [] },
"newTodo": { "type": "string", "initial": "" }
},
"actions": [
{
"name": "updateInput",
"steps": [
{
"do": "set",
"target": "newTodo",
"value": { "expr": "param", "name": "event", "path": "target.value" }
}
]
},
{
"name": "addTodo",
"steps": [
{
"do": "if",
"condition": {
"expr": "bin",
"op": "!=",
"left": { "expr": "state", "name": "newTodo" },
"right": { "expr": "lit", "value": "" }
},
"then": [
{
"do": "update",
"target": "todos",
"operation": "push",
"value": {
"expr": "lit",
"value": { "id": 1, "title": "placeholder", "completed": false }
}
},
{ "do": "set", "target": "newTodo", "value": { "expr": "lit", "value": "" } }
]
}
]
},
{
"name": "deleteTodo",
"steps": [
{
"do": "update",
"target": "todos",
"operation": "splice",
"index": { "expr": "param", "name": "index" },
"deleteCount": { "expr": "lit", "value": 1 }
}
]
}
],
"view": {
"kind": "element",
"tag": "div",
"children": [
{
"kind": "element",
"tag": "input",
"props": {
"type": { "expr": "lit", "value": "text" },
"value": { "expr": "state", "name": "newTodo" },
"onInput": { "event": "input", "action": "updateInput" },
"placeholder": { "expr": "lit", "value": "Add a todo..." }
}
},
{
"kind": "element",
"tag": "button",
"props": { "onClick": { "event": "click", "action": "addTodo" } },
"children": [{ "kind": "text", "value": { "expr": "lit", "value": "Add" } }]
},
{
"kind": "element",
"tag": "ul",
"children": [
{
"kind": "each",
"items": { "expr": "state", "name": "todos" },
"as": "todo",
"index": "i",
"key": { "expr": "var", "name": "todo", "path": "id" },
"body": {
"kind": "element",
"tag": "li",
"children": [
{ "kind": "text", "value": { "expr": "var", "name": "todo", "path": "title" } },
{
"kind": "element",
"tag": "button",
"props": {
"onClick": {
"event": "click",
"action": "deleteTodo",
"payload": { "index": { "expr": "var", "name": "i" } }
}
},
"children": [{ "kind": "text", "value": { "expr": "lit", "value": "Delete" } }]
}
]
}
}
]
}
]
}
}
```
---
## Critical Rules Summary
1. **ALL values must be expressions**: `{ "expr": "lit", "value": X }`
2. **Event handlers**: `{ "event": "E", "action": "A" }` (NOT just action name)
3. **Loop variables use `var`**, not `state`
4. **Action parameters use `param`**, not `var`
5. **Text node value MUST be expression**
6. **Match update operations to state type** (toggle for boolean, increment for number, etc.)
7. **`if` node = render control**, `cond` expression = value computationNext Steps
After pasting the specification above to your AI:
- Describe your UI requirements
- Ask the AI to generate Constela JSON
- Run
constela devto validate the code - Use compiler error messages to guide fixes
See Reference for complete API documentation.