OpenAI ChatKit: How to Build Custom Chat UIs in React

A working guide to OpenAI ChatKit: what it is, how the React SDK fits together, the integration patterns we use in production, and where it falls short.

OpenAI ChatKit: How to Build Custom Chat UIs in React

Updated May 2026. Rewritten to cover the current ChatKit React SDK, server integration patterns, and the gotchas we have hit shipping it for clients.

OpenAI ChatKit is the official React component library for building chat interfaces on top of the OpenAI Assistants and Responses APIs. It saves you the weeks of work that go into rolling your own message thread, streaming, file attachments, and tool-call rendering. We have shipped it on several client projects since the SDK launched, and this guide is the version we wish we had then.

At Osher Digital, we are a Brisbane-based AI consultancy that builds production AI applications. ChatKit is one of several front-end options we evaluate for projects that need a chat interface; this article covers what it is, how it fits together, the integration patterns we use, and the cases where we reach for something else.

It is aimed at developers and technical decision-makers building chat-style interfaces on top of LLMs. If you are still working out which model provider to commit to, our guide on building agents with Claude covers the cross-provider tradeoffs.


What Is OpenAI ChatKit

ChatKit is a React component library, distributed as @openai/chatkit-react on npm, that gives you a complete chat UI in roughly twenty lines of code. It handles message rendering, streaming, attachments, tool-call display, and conversation history. It calls into your own backend, which talks to the OpenAI API on your behalf.

The split is deliberate. ChatKit owns the UI. Your backend owns the OpenAI API key, the conversation persistence, and the business logic. This is the right boundary for production: it keeps your API key off the client and gives you a clean place to add authentication, rate limiting, and audit logging.

The SDK is bundled as a set of headless hooks plus pre-styled components. You can use the components for a fast start and customise styling, or you can use only the hooks and bring your own components when the design system demands it.


When ChatKit Is the Right Tool

ChatKit is a strong fit when:

You are committed to OpenAI as the model provider. ChatKit talks to the OpenAI Assistants and Responses APIs natively. You can adapt it to other providers (we have done this), but the path of least resistance assumes OpenAI underneath.

You want a polished chat UI fast. The default components look good out of the box. Streaming is handled. File attachments work. Tool calls render in a structured way. If your team would rather ship the AI logic than the message bubble, ChatKit removes weeks of UI work.

Your application is React. ChatKit is React-only at the time of writing. Vue, Svelte, or plain HTML projects need a different approach.

You want clear separation of concerns. The UI layer (ChatKit) and the API layer (your backend) talk over a well-defined HTTP boundary. This is the architecture we recommend for any production chat application regardless of which UI library is involved.


Installing ChatKit and the Minimal Setup

Install the React SDK with your package manager of choice:

npm install @openai/chatkit-react

# or pnpm
pnpm add @openai/chatkit-react

The minimal working example is a single component that wraps ChatWindow and a hook that connects it to your backend.

import { ChatWindow, useChatKit } from "@openai/chatkit-react";

export function SupportChat() {
  const chat = useChatKit({
    endpoint: "/api/chat",
    initialMessages: [
      { role: "assistant", content: "Hi, how can I help today?" },
    ],
  });
  return (
    <ChatWindow
      chat={chat}
      placeholder="Ask a question..."
      attachmentsEnabled
    />
  );
}

That gives you a working chat with streaming, attachments, and message history. The endpoint points at your own backend route, which is where the OpenAI API call happens.


The Server Side: The Half Most People Skip

ChatKit’s documentation focuses on the React side, which gives the impression that the SDK is the whole story. It is not. The server side is where the production complexity lives, and skipping it leads to chat applications that work locally but fall over in deployment.

The endpoint that ChatKit posts to needs to do four things: authenticate the user, persist conversation state, call the OpenAI API with streaming, and stream the response back to the client in the format ChatKit expects. Here is the pattern we use, written for a Next.js route handler:

import OpenAI from "openai";
import { NextRequest } from "next/server";
import { authenticate } from "@/lib/auth";
import { saveMessage, loadHistory } from "@/lib/conversations";

const openai = new OpenAI();

export async function POST(req: NextRequest) {
  const user = await authenticate(req);
  if (!user) return new Response("unauthorised", { status: 401 });

  const { conversationId, message } = await req.json();
  await saveMessage(conversationId, user.id, "user", message);
  const history = await loadHistory(conversationId);

  const stream = await openai.responses.create({
    model: "gpt-4.1",
    input: history,
    stream: true,
  });

  const encoder = new TextEncoder();
  const body = new ReadableStream({
    async start(controller) {
      let assistantBuffer = "";
      for await (const event of stream) {
        controller.enqueue(encoder.encode(JSON.stringify(event) + "\n"));
        if (event.type === "response.output_text.delta") {
          assistantBuffer += event.delta;
        }
      }
      await saveMessage(conversationId, user.id, "assistant", assistantBuffer);
      controller.close();
    },
  });

  return new Response(body, {
    headers: { "Content-Type": "application/x-ndjson" },
  });
}

The key things this handler does that are easy to skip in a tutorial: it authenticates before doing anything, it persists every message to your own database (not just OpenAI’s threads), and it streams the response back as newline-delimited JSON which is the format ChatKit consumes. Tool calls and attachments add additional event types but the basic pattern stays the same.


Customising the Look and Feel

ChatKit ships with a default visual style that fits most products. When you need to align with a design system, the SDK exposes both CSS-variable theming for light customisation and component overrides for full control.

The CSS-variable approach covers the common cases: brand colour, surface colour, border radius, font family. Override these on the wrapping element and the components inherit them. Most client projects we ship stop here.

For deeper customisation, the SDK exposes the headless hooks that drive the UI. useChatKit returns the messages, the streaming state, and the send function. You can render those into your own components, with your own design system, and ChatKit becomes a state-management hook rather than a UI library.

We have shipped both ends of this spectrum. For internal tools, the default ChatKit components stay. For customer-facing applications with a strong brand, the headless approach is usually worth the extra two or three days of work to get the chat UI feeling like the rest of the product.


Tool Calls and Function Rendering

One of ChatKit’s strengths is the way it renders tool calls inline in the conversation. When the assistant invokes a tool, ChatKit shows a structured panel with the tool name, the arguments, and (once available) the result. For agentic applications where the user wants to see what the assistant is doing, this is significantly more useful than a generic loading spinner.

You configure tool rendering by passing a tools prop to ChatWindow. Each tool has a name, a display label, an icon, and an optional renderer for the result. Tools that return JSON can be rendered as a table; tools that return text get a collapsible code block; tools that return images render inline.

For our OpenAI Connector Registry integration work, this rendering is what makes the difference between a usable agent UI and a confusing one. Users see what the assistant is doing, can intervene if needed, and trust the output more because the steps are visible.


Conversation Persistence and Multi-User State

By default, ChatKit does not persist conversations beyond the current browser session. For most production applications, this is not what you want. Users expect to come back to a previous conversation and pick up where they left off.

The persistence pattern we use stores conversations in our own database (PostgreSQL in most projects), keyed by user ID. Each message is inserted as it streams in. ChatKit’s initialMessages prop accepts an array, so loading a conversation is a single database query plus a render. The OpenAI Threads API offers an alternative where OpenAI persists conversations on their side; we usually skip this in favour of our own database, because the threads API ties our state to OpenAI’s lifecycle and removes flexibility we want for compliance and data export.

For multi-user applications, every database query is scoped by user ID and authentication runs at the API route. The single biggest mistake we see in chat application audits is conversations that leak between users because the conversation ID was treated as a shareable identifier rather than something tied to a specific user account.


When ChatKit Is Not the Right Choice

ChatKit is excellent for the common case. It is the wrong tool when:

You are not using OpenAI. If your model provider is Anthropic, Google, or a self-hosted Llama, ChatKit’s tight coupling to the OpenAI API shapes makes it more work than it saves. We use Vercel’s AI SDK or build the UI directly on top of the chat libraries that match the chosen provider.

Your front-end is not React. Vue, Svelte, plain HTML, or React Native projects need a different SDK. The Vercel AI SDK has broader framework support; the underlying provider SDKs (the OpenAI Node SDK, the Anthropic SDK) work in any JavaScript runtime.

You need a deeply custom UX. If the chat is a small part of a larger interactive interface (for example, an inline AI assistant in a code editor or a side panel in a CRM), the headless approach gives you control. ChatKit’s components are good defaults; they are not always the right primitives.

You want to swap providers. If model-provider portability is a project goal, an abstraction layer that supports multiple providers (Vercel AI SDK, LangChain.js) gives you that. ChatKit assumes you are committed to OpenAI.


Production Gotchas We Have Hit

A handful of issues recur on every ChatKit deployment we ship.

Streaming through corporate proxies. Some corporate networks strip Transfer-Encoding: chunked or buffer responses until they complete. The streaming UX collapses into a long pause followed by the entire response landing at once. The fix is to set explicit cache and proxy headers (Cache-Control: no-cache, no-transform) and, where the proxy is under your control, configure it to pass streams through.

Token cost runaway. Without explicit conversation length limits, a long-running chat can rack up token costs on every turn because the entire history goes back into the prompt. We cap conversations at around 30 turns, summarise older messages into a system note when the limit is approached, and surface the cost in the admin UI.

Attachment size limits. ChatKit accepts attachments client-side, but your backend has to enforce limits before sending to OpenAI. Without enforcement, a user uploading a 200 MB PDF causes a request to fail in production. We cap at 20 MB and validate file types server-side.

SSE versus newline-delimited JSON. ChatKit consumes a specific stream format (newline-delimited JSON in the current version). It is not the same as Server-Sent Events. If your framework defaults to SSE for streaming, you need to override the response format. We have spent more debugging time on this single mismatch than on any other ChatKit issue.


Cost and Latency Considerations

ChatKit itself is free. The cost is the underlying OpenAI API usage. For a typical customer-facing chat with GPT-4.1 and average conversation lengths, we see token spend land between $0.10 and $0.50 USD per active conversation.

Cost optimisation patterns we use: prompt caching where the system prompt is large and reused, summary memory for long conversations, model fallback (start with GPT-4o-mini, escalate to GPT-4.1 only when the cheaper model is uncertain). These bring spend down by 40-70% on most chat applications without a noticeable quality regression.

Latency: streaming first-token usually arrives in 300-700ms when calling OpenAI from a server in the United States. From an Australian server, the first-token latency is closer to 500-1,000ms because of the round trip. For applications where this matters (interactive coding assistants, voice transcription), we deploy the chat backend in a US region or use OpenAI’s regional endpoints when available. Book a call if this latency profile is a concern for your use case.


Frequently Asked Questions

What is OpenAI ChatKit?

ChatKit is an official React component library from OpenAI for building chat interfaces on top of the OpenAI Assistants and Responses APIs. It includes pre-built components for the message thread, streaming, file attachments, and tool calls, plus headless hooks if you want to bring your own components. It is distributed as @openai/chatkit-react on npm.

How do I install the ChatKit React SDK?

Install with npm install @openai/chatkit-react or the equivalent in your package manager. Import ChatWindow and useChatKit, point the hook at your backend endpoint, and render the component. The minimal working example is around 20 lines including imports and JSX.

Does ChatKit work with Anthropic Claude or other providers?

Not directly. ChatKit is built around the OpenAI API shapes (Assistants, Responses, tool calls). You can adapt your backend to translate between Claude or another provider and the OpenAI shape, but the path of least resistance is to use a different chat library when your model provider is not OpenAI. The Vercel AI SDK is the closest cross-provider equivalent.

Can I use ChatKit without React?

No, the SDK is React-only. For Vue, Svelte, or plain HTML applications, you would need to build the chat UI directly on top of the OpenAI SDK or use a framework-specific alternative. The patterns ChatKit implements (streaming, tool-call rendering, attachments) are not difficult to build, but ChatKit removes a couple of weeks of work for React projects specifically.

How do I style and theme ChatKit components?

For light customisation, set CSS variables on the wrapping element to control brand colour, surface colour, border radius, and typography. For deeper changes, use the headless hooks (useChatKit returns messages, streaming state, and the send function) and render your own components. The default styles work well for internal tools; the headless approach gives you full control for customer-facing UIs.

How do I persist ChatKit conversations across sessions?

Persist conversations on your own backend. Insert each message to your database as it streams, and pass the loaded history into initialMessages when the user opens an existing conversation. We use PostgreSQL keyed by user ID. The OpenAI Threads API offers an alternative where OpenAI stores the conversation, but tying your state to a vendor’s persistence layer removes flexibility that most teams want for export, audit, and compliance.

How much does it cost to run a ChatKit application?

ChatKit itself is free. The running cost is OpenAI API usage. For a typical customer-facing chat with GPT-4.1, expect $0.10 to $0.50 USD per active conversation. Prompt caching, summary memory, and tiered model selection (cheaper model for simple turns, escalate when needed) bring spend down significantly without a noticeable quality regression.

How do tool calls render in ChatKit?

Tool calls render inline in the conversation as a structured panel showing the tool name, arguments, and result. You configure rendering by passing a tools prop with each tool’s name, label, icon, and optional result renderer. JSON results render as tables, text as collapsible code blocks, images inline. This is one of ChatKit’s strengths for agentic applications where users want to see what the assistant is doing.


If you want help shipping a production chat application built on ChatKit, or if you are weighing it against alternatives like the Vercel AI SDK or a custom build, get in touch with our team. We are based in Brisbane and ship AI applications for clients across multiple sectors.

Ready to streamline your operations?

Get in touch for a free consultation to see how we can streamline your operations and increase your productivity.