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 Agent Components
Speck.js is AI-native with built-in Agent components. Add AI chat to any component with a single line:
One-Liner Chat UI
<SimpleChat>
<Agent.Chat purpose="You are a helpful assistant" />
</SimpleChat>
That's it. Full chat interface with input, submit button, streaming responses, error handling, and chat history.
Agent.Chat Props
| Prop | Type | Default | Description |
|---|---|---|---|
purpose |
string | "You are a helpful AI assistant." | System prompt |
model |
string | "claude-sonnet-4-20250514" | Claude model |
temperature |
number | 0.7 | Response randomness (0-1) |
maxTokens |
number | 1000 | Max response length |
placeholder |
string | "Type a message..." | Input placeholder |
submitLabel |
string | "Send" | Button text |
showHistory |
boolean | true | Show chat history |
Agent.Ask
For single prompt/response tasks like translation, summarization, or extraction:
Basic Usage
<Translator>
<state input={""} />
<state output={""} />
<input
value={state.input.value}
onInput={(e) => state.input.value = e.target.value}
/>
<Agent.Ask
purpose="Translate to French. Output only the translation."
autoSend={false}
onResponse={(text) => state.output.value = text}
>
{({ send, loading }) => (
<button onClick={() => send(state.input.value)}>
{loading ? "Translating..." : "Translate"}
</button>
)}
</Agent.Ask>
<p>{state.output.value}</p>
</Translator>
Agent.Ask Props
| Prop | Type | Default | Description |
|---|---|---|---|
purpose |
string | Required | System prompt |
prompt |
string | - | Message to send |
autoSend |
boolean | true | Send on mount |
onResponse |
function | - | Called with response |
onError |
function | - | Called on error |
Compound Components
For full customization, use the
<Agent> provider with sub-components:
<CustomChat>
<Agent purpose="You are a code reviewer" temperature={0.3}>
<Agent.History />
<Agent.Error />
<Agent.Response />
<Agent.Input placeholder="Paste code..." />
<Agent.Submit>Review</Agent.Submit>
<Agent.Clear>Reset</Agent.Clear>
</Agent>
</CustomChat>
Available Sub-Components
| Component | Description |
|---|---|
<Agent.Input /> |
Text input field |
<Agent.Submit> |
Submit button |
<Agent.Response /> |
Response display |
<Agent.Loading> |
Loading indicator (only shows when loading) |
<Agent.Error /> |
Error display (only shows on error) |
<Agent.History /> |
Chat history |
<Agent.Clear> |
Clear history button |
Custom Response Rendering
<Agent.Response render={(content) => <Markdown>{content}</Markdown>} />
Agent API Reference
Agent Props
| Prop | Type | Default |
|---|---|---|
purpose |
string | "You are a helpful AI assistant." |
model |
string | "claude-sonnet-4-20250514" |
temperature |
number | 0.7 |
maxTokens |
number | 1000 |
streaming |
boolean | true |
onResponse |
(text: string) => void | - |
onError |
(error: Error) => void | - |
Configuration
Add your API key to .env:
VITE_ANTHROPIC_API_KEY=sk-ant-api03-xxxxx
Migration from Old API
Before (40+ lines):
<AskClaude>
<state userMessage={""} />
<state response={""} />
<state loading={false} />
<state error={null} />
<!-- ... 30+ more lines of boilerplate ... -->
</AskClaude>
After (1 line):
<AskClaude>
<Agent.Chat purpose="You are a helpful assistant." />
</AskClaude>
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>
|
<Agent.Chat /> |
One-liner chat UI | <Agent.Chat purpose="..." /> |
<Agent.Ask> |
Single prompt/response |
<Agent.Ask
purpose="...">...</Agent.Ask>
|
<Agent> |
Custom AI chat container |
<Agent purpose="...">...</Agent>
|