> ## Documentation Index
> Fetch the complete documentation index at: https://docs.outkit.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# React SDK

> Render Outkit components in React with streaming, Shadow DOM isolation, and design tokens

The `@outkit-dev/react` package provides a drop-in React component and streaming hook for rendering Outkit-enhanced AI output. Components render inside Shadow DOM for complete style isolation from your app's CSS.

## Install

The recommended way to install is through the CLI — it detects your framework, picks a
profile, installs the SDK, and writes the backend proxy in one step:

```bash theme={null}
npm install -g @outkit-dev/cli@latest
outkit login
outkit init
```

See [Project Setup](/docs/cli/setup) for what `outkit init` writes.

To install the package manually:

```bash theme={null}
npm install @outkit-dev/react
```

Requires React 18 or 19. Works with Next.js, Vite, Remix, and any React framework.

## Architecture

Your API key must stay on your server — never expose it in client-side code:

1. Your **backend** calls `POST /render/enhance` with the API key
2. Your backend **proxies** the SSE stream to your frontend
3. Your **frontend** feeds the response to the SDK — one line of code

```
┌──────────┐    POST /render/enhance    ┌──────────┐    proxy SSE    ┌──────────┐
│  Outkit   │ ◄──────────────────────── │  Your    │ ──────────────► │  Your    │
│  API      │ ─────────────────────────► │  Server  │                │  Browser │
└──────────┘         SSE stream         └──────────┘                └──────────┘
                                                              await feedResponse(res)
                                                                        │
                                                                        ▼
                                                              ┌──────────────────┐
                                                              │   <AIRenderer>   │
                                                              │   Shadow DOM     │
                                                              └──────────────────┘
```

## Quick Start

```tsx theme={null}
import { AIRenderer, useBlockStream } from '@outkit-dev/react';

function ChatMessage({ messageId }: { messageId: string }) {
  const { blocks, design, isStreaming, feedResponse, reset } = useBlockStream();

  const enhance = async () => {
    reset();
    try {
      const response = await fetch(`/api/enhance/${messageId}`);
      await feedResponse(response);
    } catch (err) {
      console.error('Enhance failed:', err);
    }
  };

  return (
    <div>
      <button onClick={enhance}>Enhance</button>
      <AIRenderer blocks={blocks} design={design} streaming={isStreaming} theme="auto" />
    </div>
  );
}
```

That's it. `feedResponse` handles SSE parsing, design token extraction, streaming JSON parsing, and completion — all internally. Blocks update at 60fps via `requestAnimationFrame` batching. It throws on non-ok HTTP responses, so wrap it in try/catch.

<Note>
  Your `/api/enhance/:id` backend endpoint should proxy the Outkit SSE stream. See [Backend Proxy](#backend-proxy) below for examples.
</Note>

## `useBlockStream`

The streaming hook manages block state, design tokens, and batched React state updates.

```tsx theme={null}
const {
  blocks,         // ContentBlock[] — pass to <AIRenderer blocks={blocks} />
  design,         // Record<string, string> | undefined — pass to <AIRenderer design={design} />
  isStreaming,    // boolean — pass to <AIRenderer streaming={isStreaming} />
  streamState,    // 'idle' | 'streaming' | 'done' | 'error' | 'destroyed'

  // Tier 1 — Full automation (recommended)
  feedResponse,   // (response: Response) => Promise<{ blocks, design }>

  // Tier 2 — Raw SSE bytes
  feedSSE,        // (rawText: string) => void

  // Tier 3 — One unwrapped event payload
  feedEvent,      // (data: string) => void

  // Tier 4 — Full manual control
  feedChunk,      // (data: string) => void
  feedMeta,       // (tokens: Record<string, string>) => void
  feedDone,       // () => void

  reset,          // () => void — clear everything, abort in-flight streams
} = useBlockStream();
```

### Choosing a Tier

Pick the tier that matches how your backend delivers data to the browser:

| Tier  | Method                                | When to use                                                                    |
| ----- | ------------------------------------- | ------------------------------------------------------------------------------ |
| **1** | `feedResponse(res)`                   | Your backend proxies the Outkit SSE stream as-is. **Start here.**              |
| **2** | `feedSSE(text)`                       | You're reading the response body yourself and want to forward raw chunks.      |
| **3** | `feedEvent(data)`                     | You receive individual events via WebSocket, Socket.IO, or a custom transport. |
| **4** | `feedChunk` / `feedMeta` / `feedDone` | You need full manual control over every event type.                            |

Each tier calls the one below it internally. `feedResponse` calls `feedSSE`, which calls `feedEvent`, which calls `feedChunk`/`feedMeta`/`feedDone`.

### Tier 1: `feedResponse(response)` — Recommended

The simplest integration. Pass a `fetch` Response and the SDK handles everything:

```tsx theme={null}
const enhance = async () => {
  reset();
  try {
    const response = await fetch('/api/enhance/123');
    const { blocks, design } = await feedResponse(response);
    // blocks and design are also available via the hook's reactive state,
    // but the return value is useful for post-stream logic:
    console.log(`Rendered ${blocks.length} blocks`);
  } catch (err) {
    console.error('Stream failed:', err);
  }
};
```

* Reads the SSE stream, parses `data:` lines, extracts design tokens, accumulates LLM chunks, and signals completion
* **Returns `{ blocks, design }`** — the final parsed blocks and design tokens after the stream completes
* Auto-resets if a previous stream is in progress (safe for rapid re-triggers)
* Throws on non-ok HTTP responses (catch and show your own error UI)
* Abortable via `reset()` — cancels the in-flight reader

### Tier 2: `feedSSE(rawText)`

For when you're reading the response body yourself:

```tsx theme={null}
const reader = response.body.getReader();
const decoder = new TextDecoder();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  feedSSE(decoder.decode(value, { stream: true }));
}
```

Handles SSE framing (`data:` prefix stripping, `\n\n` event boundaries, fragment buffering across TCP chunks).

### Tier 3: `feedEvent(data)`

For WebSocket or custom transports delivering one event at a time:

```tsx theme={null}
socket.on('outkit-event', (data: string) => {
  feedEvent(data);  // handles [DONE], meta routing, and LLM chunk delegation
});
```

### Tier 4: `feedChunk` / `feedMeta` / `feedDone`

Full manual control — you parse SSE yourself and call the right method:

```tsx theme={null}
for (const line of sseLines) {
  if (!line.startsWith('data: ')) continue;
  const data = line.slice(6);

  if (data === '[DONE]') {
    feedDone();
    return;
  }

  try {
    const parsed = JSON.parse(data);
    if (parsed.type === 'meta' && parsed.design) {
      feedMeta(parsed.design);
      continue;
    }
  } catch { /* raw LLM token */ }

  feedChunk(data);
}
```

* **`feedChunk(data)`** — Appends to an internal JSON buffer, runs the streaming parser, and updates blocks.
* **`feedMeta(tokens)`** — Sets design tokens. Flushes any blocks that were buffered waiting for design.
* **`feedDone()`** — Final parse of the accumulated buffer, flush pending blocks, mark stream complete.

### `reset()`

Clears all state: blocks, design tokens, streaming flag, and aborts any in-flight `feedResponse`. Call before starting a new stream.

## `<AIRenderer>`

Renders `ContentBlock[]` inside Shadow DOM with full style isolation.

### Props

| Prop                  | Type                                                  | Default     | Description                                                                  |
| --------------------- | ----------------------------------------------------- | ----------- | ---------------------------------------------------------------------------- |
| `blocks`              | `ContentBlock[]`                                      | —           | Array of content blocks from the stream                                      |
| `design`              | `Record<string, string>`                              | —           | CSS custom properties from your design profile                               |
| `streaming`           | `boolean`                                             | `false`     | Enables streaming cursor, auto-skeletons, and entry animations               |
| `theme`               | `"light" \| "dark" \| "auto"`                         | `"auto"`    | Color theme. `"auto"` detects `prefers-color-scheme`                         |
| `loading`             | `boolean`                                             | `false`     | Show skeleton placeholder instead of content                                 |
| `skeletonType`        | `"table" \| "card" \| "chart" \| "text" \| "generic"` | `"generic"` | Skeleton variant when `loading` is true                                      |
| `confidenceThreshold` | `number`                                              | `0.7`       | Minimum confidence to render a component. Below this, fallback text is shown |
| `className`           | `string`                                              | —           | Class applied to the wrapper `<div>`                                         |
| `onInteraction`       | `(event: OutkitInteractionEvent) => void`             | —           | Callback for interactive events (table sort, checkbox toggle, etc.)          |
| `spec`                | `ComponentBlock`                                      | —           | Render a single component instead of a blocks array                          |
| `placeholder`         | `React.ReactNode`                                     | —           | Content shown when no blocks or spec provided                                |

### Minimal Usage

```tsx theme={null}
<AIRenderer blocks={blocks} />
```

### Full Usage

```tsx theme={null}
<AIRenderer
  blocks={blocks}
  design={design}
  streaming={isStreaming}
  theme="auto"
  confidenceThreshold={0.7}
  onInteraction={(event) => console.log(event)}
/>
```

### Theme Behavior

* `"auto"` — Watches `prefers-color-scheme` media query and updates dynamically
* `"light"` / `"dark"` — Forces a specific mode regardless of system preference

Design tokens with `-dark` suffix (e.g. `--outkit-text-dark`) are automatically swapped in when the resolved theme is dark.

## Design Tokens

Design tokens are CSS custom properties injected into the Shadow DOM. They come from the `meta` SSE event during streaming or the `design` field in JSON mode.

When using `feedResponse` or any higher-tier method, tokens are extracted and applied automatically — you just pass `design` to `<AIRenderer>`.

```tsx theme={null}
<AIRenderer
  blocks={blocks}
  design={design}  // ← from useBlockStream(), applied automatically
/>
```

You can also pass tokens manually:

```tsx theme={null}
<AIRenderer
  blocks={blocks}
  design={{
    '--outkit-primary': '#b30069',
    '--outkit-bg': '#fbf9f8',
    '--outkit-text': '#1b1c1c',
    '--outkit-font': '"Inter", system-ui, sans-serif',
  }}
/>
```

### Available Tokens

| Token                 | Description                        | Default           |
| --------------------- | ---------------------------------- | ----------------- |
| `--outkit-font`       | Font family                        | System font stack |
| `--outkit-text`       | Primary text color                 | `#0f172a`         |
| `--outkit-text-muted` | Secondary text color               | `#64748b`         |
| `--outkit-bg`         | Background                         | `#ffffff`         |
| `--outkit-bg-subtle`  | Subtle background (headers, code)  | `#f8fafc`         |
| `--outkit-bg-hover`   | Hover state background             | `#f1f5f9`         |
| `--outkit-border`     | Border color                       | `#e2e8f0`         |
| `--outkit-radius`     | Border radius                      | `8px`             |
| `--outkit-primary`    | Primary accent (links, highlights) | `#3b82f6`         |
| `--outkit-success`    | Success state                      | `#16a34a`         |
| `--outkit-warning`    | Warning state                      | `#d97706`         |
| `--outkit-error`      | Error state                        | `#dc2626`         |

## Backend Proxy

Your backend proxies the Outkit API so your API key never reaches the browser.

<CodeGroup>
  ```typescript Express / Node.js theme={null}
  import express from 'express';

  const app = express();

  app.post('/api/enhance/:messageId', async (req, res) => {
    const aiText = await getMessageText(req.params.messageId);

    const outkitResponse = await fetch('https://api.outkit.dev/render/enhance', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-outkit-api-key': process.env.OUTKIT_API_KEY!,
      },
      body: JSON.stringify({
        content: aiText,
        context: 'User chat message',
      }),
    });

    if (!outkitResponse.ok || !outkitResponse.body) {
      return res.status(outkitResponse.status).end();
    }

    // Pipe the SSE stream to the client
    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');

    const reader = outkitResponse.body.getReader();
    try {
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        res.write(value);
      }
    } finally {
      res.end();
    }
  });
  ```

  ```python FastAPI theme={null}
  from fastapi import FastAPI
  from fastapi.responses import StreamingResponse
  import httpx
  import os

  app = FastAPI()

  @app.post("/api/enhance/{message_id}")
  async def enhance(message_id: str):
      ai_text = await get_message_text(message_id)

      async def proxy_stream():
          async with httpx.AsyncClient() as client:
              async with client.stream(
                  "POST",
                  "https://api.outkit.dev/render/enhance",
                  headers={
                      "Content-Type": "application/json",
                      "x-outkit-api-key": os.environ["OUTKIT_API_KEY"],
                  },
                  json={"content": ai_text},
              ) as response:
                  async for chunk in response.aiter_bytes():
                      yield chunk

      return StreamingResponse(proxy_stream(), media_type="text/event-stream")
  ```

  ```typescript Next.js Route Handler theme={null}
  // app/api/enhance/[messageId]/route.ts
  export async function POST(
    request: Request,
    { params }: { params: { messageId: string } }
  ) {
    const aiText = await getMessageText(params.messageId);

    const response = await fetch('https://api.outkit.dev/render/enhance', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-outkit-api-key': process.env.OUTKIT_API_KEY!,
      },
      body: JSON.stringify({ content: aiText }),
    });

    return new Response(response.body, {
      headers: { 'Content-Type': 'text/event-stream' },
    });
  }
  ```
</CodeGroup>

## SSE Event Types

When streaming from `/render/enhance`, the API sends these SSE events:

| Event       | Format                           | Description                                    |
| ----------- | -------------------------------- | ---------------------------------------------- |
| Design meta | `{"type":"meta","design":{...}}` | Design tokens for your profile. Always first.  |
| Picker meta | `{"type":"meta","picker":{...}}` | Internal classification metadata. Skip this.   |
| LLM chunks  | Raw JSON tokens                  | Partial JSON structure streamed token-by-token |
| Done        | `[DONE]`                         | Stream complete signal                         |

<Note>
  When using `feedResponse` (Tier 1) or `feedSSE` (Tier 2), you don't need to handle these events manually — the SDK parses them for you.
</Note>

## Framework-Agnostic Core

The streaming protocol handler is available as a standalone package for non-React integrations:

```bash theme={null}
npm install @outkit-dev/core
```

See the [@outkit-dev/core reference](/docs/sdk/core) for using `OutkitStream` with Vue, Svelte, vanilla JS, or Node.js.

## Streaming Utilities

For advanced integrations, the low-level parsing functions are also exported:

```ts theme={null}
import {
  parseStreamingBlocks,
  completePartialJson,
  expandWireBlock,
} from '@outkit-dev/react';
```

### `parseStreamingBlocks(accumulated: string): ContentBlock[]`

Takes the full accumulated raw LLM output, closes unclosed JSON structures, parses, expands wire format, and returns all currently renderable blocks.

### `completePartialJson(partial: string): string`

Closes unclosed strings, arrays, and objects so partial JSON can be parsed. Used internally by `parseStreamingBlocks`.

### `expandWireBlock(block: Record<string, unknown>): Record<string, unknown>`

Expands compact wire format (`{ c, v, p }`) to full format (`{ type, component, version, props }`). The renderer does this at render time automatically.
