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>