Documentation
Everything you need to build with Speck.js, the AI-native web framework.
Quick Start
Installation
$ npm create speck-app my-app
$ cd my-app
$ npm run dev
Your First Component
Create src/components/Hello.speck:
<Hello>
<state name={"World"} />
<div>
<h1>Hello, {state.name.value}!</h1>
<input
value={state.name.value}
onInput={(e) => state.name.value = e.target.value}
/>
</div>
</Hello>
Use it anywhere—no imports needed:
<App>
<Hello />
</App>
Project Structure
my-speck-app/
├── src/
│ ├── components/ # Your .speck components
│ │ ├── App.speck
│ │ ├── Header.speck
│ │ └── Counter.speck
│ ├── .compiled/ # Auto-generated (don't edit)
│ │ ├── App.jsx
│ │ └── _componentRegistry.js
│ └── lib/
│ └── agent-runtime.js # AI agent runtime
├── api/
│ └── chat.js # AI proxy server
├── compiler/
│ └── compiler.js # Speck compiler
├── .env # API keys (git-ignored)
├── package.json
└── vite.config.js
CLI Commands
| Command | Description |
|---|---|
npm run dev |
Start dev server + compiler + AI proxy |
npm run dev:no-agent |
Start without AI proxy |
npm run build |
Production build |
npm run compile |
One-time compile all components |
npm run watch |
Watch for .speck changes |
Components
Basic Component
Every component is defined by its filename. A file named Button.speck creates a <Button> component:
<Button>
<button style="padding: 10px 20px; background: #7c3aed;">
Click me
</button>
</Button>
Using Components
Components auto-discover each other. No imports needed:
<App>
<Header />
<main>
<Button />
<Counter />
</main>
<Footer />
</App>
State Management
Declaring State
Use <state> tags to declare reactive state. Each state variable needs its own tag:
<Counter>
<state count={0} />
<state name={"Counter"} />
<state isActive={true} />
<state items={[]} />
<div>
<p>Count: {state.count.value}</p>
</div>
</Counter>
Reading State
Access state values with .value:
<p>Current count: {state.count.value}</p>
<p>User name: {state.user.value?.name}</p>
Updating State
Assign to .value to trigger reactive updates:
<button onClick={() => state.count.value++}>Increment</button>
<button onClick={() => state.count.value = 0}>Reset</button>
Script Blocks
Use <script> for complex logic:
<TodoList>
<state todos={[]} />
<state newTodo={""} />
<script>
const addTodo = () => {
if (!state.newTodo.value.trim()) return;
state.todos.value = [...state.todos.value, {
id: Date.now(),
text: state.newTodo.value,
done: false
}];
state.newTodo.value = "";
};
const toggleTodo = (id) => {
state.todos.value = state.todos.value.map(t =>
t.id === id ? {...t, done: !t.done} : t
);
};
</script>
<div>
<input value={state.newTodo.value} />
<button onClick={addTodo}>Add</button>
</div>
</TodoList>
Conditionals
<if condition={state.isLoggedIn.value}>
<p>Welcome back!</p>
</if>
<if condition={!state.isLoggedIn.value}>
<p>Please log in.</p>
</if>
<if condition={state.count.value > 10}>
<p>Count is greater than 10!</p>
</if>
Loops
<loop of={state.items.value} let={item}>
<div>
<h3>{item.title}</h3>
<p>{item.description}</p>
</div>
</loop>
Switch/Case
<switch on={state.status.value}>
<case when="loading">
<p>Loading...</p>
</case>
<case when="success">
<p>Data loaded!</p>
</case>
<case when="error">
<p>Something went wrong.</p>
</case>
</switch>
Routing
Basic Router
<App>
<Router>
<route path="/">
<HomePage />
</route>
<route path="/about">
<AboutPage />
</route>
</Router>
</App>
Dynamic Routes
<route path="/user/:id" let={params}>
<UserProfile userId={params.id} />
</route>
<route path="/posts/:category/:slug" let={params}>
<BlogPost category={params.category} slug={params.slug} />
</route>
Async Data
<UserList>
<async promise={fetch('/api/users').then(r => r.json())}>
<loading>
<p>Loading users...</p>
</loading>
<then let={users}>
<loop of={users} let={user}>
<div>{user.name}</div>
</loop>
</then>
<catch let={error}>
<p>Error: {error.message}</p>
</catch>
</async>
</UserList>
Re-fetch on Change
Use the key attribute to re-fetch when a value changes:
<async promise={fetch(`/api/users/${state.userId.value}`)} key={state.userId.value}>
...
</async>
Props
Receiving Props
<Button>
<props label onClick disabled />
<button onClick={onClick} disabled={disabled}>
{label}
</button>
</Button>
<!-- Usage -->
<Button label="Click Me" onClick={() => alert('Clicked!')} />
Spread Props
<Input>
<props />
<input {...props} />
</Input>
<!-- Usage -->
<Input type="email" placeholder="Enter email" />
Slots
Default Slot
<Card>
<div class="card">
<slot />
</div>
</Card>
<!-- Usage -->
<Card>
<h2>Card Title</h2>
<p>This content goes in the slot.</p>
</Card>
Named Slots
<Modal>
<header>
<slot name="header" />
</header>
<main>
<slot />
</main>
<footer>
<slot name="footer" />
</footer>
</Modal>
Lifecycle
onMount
Run code when component mounts:
<Dashboard>
<state data={null} />
<onMount>
{fetch('/api/dashboard').then(r => r.json()).then(d => state.data.value = d)}
</onMount>
<if condition={state.data.value}>
<DashboardContent data={state.data.value} />
</if>
</Dashboard>
AI with SpeckAgent
Speck.js is AI-native with the built-in SpeckAgent class. Add AI chat to any component:
<AskClaude>
<state userMessage={""} />
<state response={""} />
<state loading={false} />
<state error={null} />
<input
value={state.userMessage.value}
onInput={(e) => state.userMessage.value = e.target.value}
placeholder="Ask me anything..."
/>
<button
onClick={async () => {
if (!state.userMessage.value.trim()) return;
state.loading.value = true;
state.error.value = null;
try {
const agent = new window.SpeckAgent({
purpose: 'You are a helpful assistant.',
model: 'claude-sonnet-4-20250514',
provider: 'anthropic',
streaming: false
});
const result = await agent.send(state.userMessage.value);
state.response.value = result.content;
state.userMessage.value = '';
} catch (err) {
state.error.value = err.message;
}
state.loading.value = false;
}}
>
{state.loading.value ? 'Thinking...' : 'Send'}
</button>
<if condition={state.error.value}>
<p style="color: red;">{state.error.value}</p>
</if>
<if condition={state.response.value}>
<div>
<strong>Claude:</strong>
<p>{state.response.value}</p>
</div>
</if>
</AskClaude>
SpeckAgent API
const agent = new window.SpeckAgent({
// Required
purpose: "System prompt describing the AI's role",
// Optional (with defaults)
model: "claude-sonnet-4-20250514", // Model to use
provider: "anthropic", // API provider
temperature: 0.7, // Response randomness (0-1)
maxTokens: 1000, // Max response length
streaming: true, // Stream responses
});
// Send a message
const result = await agent.send("Hello!", {
history: [], // Previous messages for context
onChunk: (chunk) => {
// Called for each streamed chunk
console.log(chunk);
}
});
console.log(result.content); // AI response text
Configuration
Add your API key to .env:
VITE_ANTHROPIC_API_KEY=sk-ant-api03-xxxxx
API Reference
Special Tags
| Tag | Purpose | Example |
|---|---|---|
<state> |
Declare reactive state | <state count={0} /> |
<script> |
Component logic | <script>const fn = () => {}</script> |
<if> |
Conditional rendering | <if condition={expr}>...</if> |
<loop> |
List rendering | <loop of={arr} let={item}>...</loop> |
<switch> |
Switch statement | <switch on={val}><case when="x">...</case></switch> |
<Router> |
Client-side routing | <Router><route path="/">...</route></Router> |
<route> |
Route definition | <route path="/user/:id" let={params}>...</route> |
<async> |
Async data handling | <async promise={...}><then>...</then></async> |
<props> |
Declare accepted props | <props name onClick /> |
<slot> |
Content insertion point | <slot /> or <slot name="header" /> |
<onMount> |
Lifecycle hook | <onMount>{...code...}</onMount> |