Theming and customization
On this page
Lesson rewritten November 3, 2026. The previous version relied on components and options that do not exist in ChatKit:
ChatProvider,MessageInput, custom message rendering, and injected stylesheets. It now follows the OpenAI Theming and customization guide and the types from@openai/chatkit1.9.0; the complete example has been verified via TypeScript compilation. Two names differ between the guide and the published package, and this lesson follows the package:label(notname) for start screen suggestions, andleftActionandrightAction(notcustomButtonLeftandcustomButtonRight) for header buttons. Sources: Theming and customization in ChatKit ·ChatKitOptionsreference · ChatKit Studio.
A single options object
All ChatKit customization happens through a single options object. In React, you pass it to useChatKit({...}); with the web component, to chatkit.setOptions({...}). The shape is identical in both cases, so what you learn here applies to every integration.
One consequence follows from the architecture covered in the previous lesson: ChatKit renders inside an iframe, so your stylesheet cannot reach it. Your CSS rules apply only to the container—the size and position you give to <ChatKit className="…" />. Everything else is configured via options. This is good news for maintenance: a ChatKit update cannot break CSS selectors that do not exist.
To try options without writing code, the ChatKit Studio playground shows live rendering.
The theme
The theme controls the overall appearance: color scheme, accent color, corner radius, density, and typography.
const { control } = useChatKit({
api: { url: '/chatkit', domainKey: 'local-dev' },
theme: {
colorScheme: 'dark',
color: {
accent: { primary: '#2D8CFF', level: 2 },
},
radius: 'round',
density: 'compact',
typography: { fontFamily: "'Inter', sans-serif", baseSize: 16 },
},
});
Each setting has a closed set of values, which TypeScript validates for you:
| Option | Values | Effect |
|---|---|---|
colorScheme | 'light', 'dark' | Light (default) or dark theme |
color.accent | primary (hex, rgb(), hsl()…), level from 0 to 3 | Interface accent color and palette intensity |
color.grayscale | hue in degrees, tint from 0 to 9, shade from −4 to 4 | Hue and shade of the gray palette |
color.surface | background, foreground | Surface colors: background and foreground (text, icons) |
radius | 'pill', 'round', 'soft', 'sharp' | Corner radius for bubbles, buttons, and fields |
density | 'compact', 'normal', 'spacious' | Overall spacing |
typography | fontFamily, fontFamilyMono, baseSize (14 to 18), fontSources | Fonts and base size; fontSources loads a web font (family, src, weight…) |
To follow the user’s preference, choose the scheme when building the options, for example with window.matchMedia('(prefers-color-scheme: dark)').matches. A theme reduced to a string, theme: 'dark', is also accepted when you only need to set the color scheme.
The start screen
An empty thread can feel intimidating. The start screen replaces it with a greeting and suggestions that show the user what the assistant can do:
startScreen: {
greeting: 'Hello! How can I help you?',
prompts: [
{ label: 'Track an order', prompt: 'Where is my latest order?', icon: 'search' },
{ label: 'Open a ticket', prompt: 'I would like to report an issue.', icon: 'write' },
],
},
label is the button text, prompt is the message sent when the user clicks, and icon is a name from the ChatKit icon list (search, write, calendar, lightbulb…), or a Lucide icon in the form 'lucide:name'.
The composer
The composer object configures the placeholder text, tools offered to the user, and attachments:
composer: {
placeholder: 'Ask a question about your order…',
tools: [
{ id: 'order-search', label: 'Search order', icon: 'search', pinned: true },
],
attachments: {
enabled: true,
maxCount: 3,
maxSize: 20 * 1024 * 1024, // 20 MB per file
accept: { 'application/pdf': ['.pdf'], 'image/*': ['.png', '.jpg'] },
},
},
The tool selected from the menu is sent to the server as a tool preference for the next turn; you must account for it in your agent.
Attachments are disabled by default, and enabling them requires server-side work. With your own server, you must also specify in api how files are uploaded: uploadStrategy: { type: 'direct', uploadUrl: '…' } (the interface posts the file to your endpoint) or { type: 'two_phase' } (it requests a signed URL, then uploads the file to your storage). On the Python side, you then provide an AttachmentStore. Do not check enabled: true until this pipeline exists.
Header, history, and responses
header: {
title: { text: 'Order Assistant' },
leftAction: { icon: 'settings-cog', onClick: () => openSettings() },
},
history: { enabled: true, showRename: true, showDelete: false },
threadItemActions: { feedback: true, retry: true },
disclaimer: { text: "Responses may contain errors. [Our terms](/terms)" },
headerdisplays a fixed title (otherwise the current thread title) and up to two custom buttons,leftActionandrightAction, each with a header icon (settings-cog,home…) and a function.header: { enabled: false }hides the entire header when your application already provides its own bar.historyenables the panel of past conversations, with or without renaming and deletion. Disable it when the concept of a thread makes no sense for your use case, for example for a support assistant without user accounts.threadItemActions.feedbackdisplays thumbs up and down buttons under responses. Each click arrives at your server in theadd_feedbackmethod of yourChatKitServer(thread_id,item_ids,feedbackset to'positive'or'negative'). This is the raw material for your evaluations.retryadds the button that regenerates a response.disclaimerdisplays a short Markdown text below the composer.
Language
By default, ChatKit uses the browser’s language. If your application has its own language setting, enforce it with locale: 'en', or a regional variant like 'en-US' or 'en-GB'. Every request sent to your server carries this language in the Accept-Language header: use it to make the agent respond and to translate your tool messages in the same language as the interface.
Putting it all together
Here is a complete configuration, compiled as-is with @openai/chatkit-react 1.6.1:
import { ChatKit, useChatKit } from '@openai/chatkit-react';
export function OrderAssistant({ openSettings }: { openSettings: () => void }) {
const dark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const { control } = useChatKit({
api: { url: '/chatkit', domainKey: 'local-dev' },
locale: 'en',
theme: {
colorScheme: dark ? 'dark' : 'light',
color: { accent: { primary: '#2D8CFF', level: 2 } },
radius: 'round',
density: 'normal',
},
startScreen: {
greeting: 'Hello! How can I help you?',
prompts: [
{ label: 'Track an order', prompt: 'Where is my latest order?', icon: 'search' },
{ label: 'Open a ticket', prompt: 'I would like to report an issue.', icon: 'write' },
],
},
composer: { placeholder: 'Ask a question about your order…' },
header: {
title: { text: 'Order Assistant' },
leftAction: { icon: 'settings-cog', onClick: openSettings },
},
history: { enabled: true, showRename: true, showDelete: false },
threadItemActions: { feedback: true, retry: true },
});
return <ChatKit control={control} className="h-[640px] w-full max-w-md" />;
}
Key takeaways
- All customization happens through a single options object, identical in React (
useChatKit) and with the web component (setOptions) - ChatKit renders in an iframe: your CSS only styles the container; everything else goes through
theme theme:colorScheme,color.accent,radius,density,typography, with closed values validated by TypeScriptstartScreen(greeting and suggestions),composer(placeholder, tools, attachments),header,history,threadItemActions,disclaimer, andlocalecomplete the interface- Attachments and user feedback require server-side support:
uploadStrategyandAttachmentStore, plus theadd_feedbackmethod