Skip to main content
Lesson 18 of 25 · Building OpenAI Agents

Theming and customization

Lesson 18 of 25 Updated November 3, 2026 Developer
On this page
  1. A single options object
  2. The theme
  3. The start screen
  4. The composer
  5. Header, history, and responses
  6. Language
  7. Putting it all together
  8. Key takeaways

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/chatkit 1.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 (not name) for start screen suggestions, and leftAction and rightAction (not customButtonLeft and customButtonRight) for header buttons. Sources: Theming and customization in ChatKit · ChatKitOptions reference · 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:

OptionValuesEffect
colorScheme'light', 'dark'Light (default) or dark theme
color.accentprimary (hex, rgb(), hsl()…), level from 0 to 3Interface accent color and palette intensity
color.grayscalehue in degrees, tint from 0 to 9, shade from −4 to 4Hue and shade of the gray palette
color.surfacebackground, foregroundSurface colors: background and foreground (text, icons)
radius'pill', 'round', 'soft', 'sharp'Corner radius for bubbles, buttons, and fields
density'compact', 'normal', 'spacious'Overall spacing
typographyfontFamily, fontFamilyMono, baseSize (14 to 18), fontSourcesFonts 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)" },
  • header displays a fixed title (otherwise the current thread title) and up to two custom buttons, leftAction and rightAction, 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.
  • history enables 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.feedback displays thumbs up and down buttons under responses. Each click arrives at your server in the add_feedback method of your ChatKitServer (thread_id, item_ids, feedback set to 'positive' or 'negative'). This is the raw material for your evaluations. retry adds the button that regenerates a response.
  • disclaimer displays 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 TypeScript
  • startScreen (greeting and suggestions), composer (placeholder, tools, attachments), header, history, threadItemActions, disclaimer, and locale complete the interface
  • Attachments and user feedback require server-side support: uploadStrategy and AttachmentStore, plus the add_feedback method