Skip to main content

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.

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:
npm install -g @outkit-dev/cli@latest
outkit login
outkit init
See Project Setup for what outkit init writes. To install the package manually:
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

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.
Your /api/enhance/:id backend endpoint should proxy the Outkit SSE stream. See Backend Proxy below for examples.

useBlockStream

The streaming hook manages block state, design tokens, and batched React state updates.
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:
TierMethodWhen to use
1feedResponse(res)Your backend proxies the Outkit SSE stream as-is. Start here.
2feedSSE(text)You’re reading the response body yourself and want to forward raw chunks.
3feedEvent(data)You receive individual events via WebSocket, Socket.IO, or a custom transport.
4feedChunk / feedMeta / feedDoneYou 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. The simplest integration. Pass a fetch Response and the SDK handles everything:
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:
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:
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:
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

PropTypeDefaultDescription
blocksContentBlock[]Array of content blocks from the stream
designRecord<string, string>CSS custom properties from your design profile
streamingbooleanfalseEnables streaming cursor, auto-skeletons, and entry animations
theme"light" | "dark" | "auto""auto"Color theme. "auto" detects prefers-color-scheme
loadingbooleanfalseShow skeleton placeholder instead of content
skeletonType"table" | "card" | "chart" | "text" | "generic""generic"Skeleton variant when loading is true
confidenceThresholdnumber0.7Minimum confidence to render a component. Below this, fallback text is shown
classNamestringClass applied to the wrapper <div>
onInteraction(event: OutkitInteractionEvent) => voidCallback for interactive events (table sort, checkbox toggle, etc.)
specComponentBlockRender a single component instead of a blocks array
placeholderReact.ReactNodeContent shown when no blocks or spec provided

Minimal Usage

<AIRenderer blocks={blocks} />

Full Usage

<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>.
<AIRenderer
  blocks={blocks}
  design={design}  // ← from useBlockStream(), applied automatically
/>
You can also pass tokens manually:
<AIRenderer
  blocks={blocks}
  design={{
    '--outkit-primary': '#b30069',
    '--outkit-bg': '#fbf9f8',
    '--outkit-text': '#1b1c1c',
    '--outkit-font': '"Inter", system-ui, sans-serif',
  }}
/>

Available Tokens

TokenDescriptionDefault
--outkit-fontFont familySystem font stack
--outkit-textPrimary text color#0f172a
--outkit-text-mutedSecondary text color#64748b
--outkit-bgBackground#ffffff
--outkit-bg-subtleSubtle background (headers, code)#f8fafc
--outkit-bg-hoverHover state background#f1f5f9
--outkit-borderBorder color#e2e8f0
--outkit-radiusBorder radius8px
--outkit-primaryPrimary accent (links, highlights)#3b82f6
--outkit-successSuccess state#16a34a
--outkit-warningWarning state#d97706
--outkit-errorError state#dc2626

Backend Proxy

Your backend proxies the Outkit API so your API key never reaches the browser.
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();
  }
});

SSE Event Types

When streaming from /render/enhance, the API sends these SSE events:
EventFormatDescription
Design meta{"type":"meta","design":{...}}Design tokens for your profile. Always first.
Picker meta{"type":"meta","picker":{...}}Internal classification metadata. Skip this.
LLM chunksRaw JSON tokensPartial JSON structure streamed token-by-token
Done[DONE]Stream complete signal
When using feedResponse (Tier 1) or feedSSE (Tier 2), you don’t need to handle these events manually — the SDK parses them for you.

Framework-Agnostic Core

The streaming protocol handler is available as a standalone package for non-React integrations:
npm install @outkit-dev/core
See the @outkit-dev/core reference for using OutkitStream with Vue, Svelte, vanilla JS, or Node.js.

Streaming Utilities

For advanced integrations, the low-level parsing functions are also exported:
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.