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>