Flutter docs
Default skin

Theming

Colour resolves in CSS, before React runs. A Theme is the light or dark rendering of the active Skin; a Skin is one CSS file that rewrites brand tokens and nothing else.

Selecting a theme

data-theme on the document root wins in both directions; with no attribute the page follows prefers-color-scheme. CSS remains the source of truth. ThemeProvider writes the attribute and gives Ant a generated seed from those same Tokens so server and client use the same palette.

The provider belongs at the root of an application, so it cannot be staged inside an example. The toggle at the top right of this page is this exact hook.

tsx
import { ThemeProvider, useTheme } from '@ceebee/ui/client';import { getThemeBootstrapScript } from '@ceebee/ui';
export default function Layout({ children }) {  return (    <html suppressHydrationWarning>      <head><script dangerouslySetInnerHTML={{ __html: getThemeBootstrapScript() }} /></head>      <body><ThemeProvider defaultChoice="system">{children}</ThemeProvider></body>    </html>  );}
function ThemeToggle() {  const { choice, setChoice, resolved } = useTheme();  return <Button onClick={() => setChoice(resolved === 'dark' ? 'light' : 'dark')}>{resolved}</Button>;}

For a saved browser choice to affect the very first paint, place getThemeBootstrapScript() in the document head before stylesheets. The fixed script restores the cb-theme choice before CSS paints: it sets data-theme for an explicit choice, removes it for system, and writes the resolved data-cb-theme-mode for integrations that need the browser's initial light or dark mode. Storage or browser API failures leave the server-rendered theme in place. suppressHydrationWarning on the document root accounts for the intentional pre-hydration attribute update.

The browser's own chrome

The library declares color-scheme for each theme, with the same selectors the tokens use, so what the browser draws itself follows the theme: scrollbars, native pickers, autofill. Without it a dark theme still gets the browser's light scrollbars — a white track beside dark surfaces. Scrollbars take one colour, the theme's accent (--cb-tone-brand), with no track; under forced-colors they are the system's again.

A scroll container that should only show its scrollbar while it is being scrolled (a navigation rail, a short list) can clear scrollbar-color at rest and restore it while scrolling — that is the product's choice per container, not a global default, because a long page's scrollbar is a cue that there is more.

Server rendering Ant controls

An application that extracts Ant styles during server rendering must pass the resolved mode that the server knew. ThemeProvider writes that non-sensitive mode to cb-theme-mode; the next request can read it with the framework-neutral helper from the server-safe package entry. The values are generated inside CeeBee—an application never copies raw colours or Ant tokens.

tsx
// Server Component. In Next.js 15+, headers() is asynchronous.import { headers } from 'next/headers';import { readThemeModeCookie } from '@ceebee/ui';import { ThemeProvider } from '@ceebee/ui/client';
export default async function Layout({ children }) {  const requestHeaders = await headers();  const initialMode = readThemeModeCookie(requestHeaders.get('cookie')) ?? 'light';
  return (    <html data-theme={initialMode}>      <body>        <ThemeProvider initialMode={initialMode} skin="clarity">          {children}        </ThemeProvider>      </body>    </html>  );}

Set the same mode on the server-rendered document root; otherwise the generated Ant seed and the CSS Skin can disagree during the first paint. The skin prop must match the Skin stylesheet imported by the application. The default Skin is ceebee; the other generated choices are astra, clarity, and moodboard. On a reader's first request there may be no cookie yet. The application chooses its default for that request, while the zero-specificity pre-paint layer keeps controls readable until hydration; subsequent requests carry the browser's resolved mode and can safely use the framework's Ant style-extraction adapter.

Language of the runtime's own strings

An empty table's "No data", a date picker's "Select date", pagination: the component runtime draws those itself, in English unless told otherwise. Pass one of the locale exports to ThemeProvider and they follow the product's language — including in static dialogs, which render outside the tree:

tsx
import { ThemeProvider, idIDLocale } from '@ceebee/ui/client';
<ThemeProvider skin="clarity" locale={idIDLocale}>{children}</ThemeProvider>

The library's own strings — overlays, carousel, pagination summaries — are a separate contract, set through LabelsProvider.

getCeebeeAntThemeSeed is also exported for framework adapters that need the generated seed directly. It returns a fresh object for one Skin, mode, and contrast preference.

The style cache must come from CeeBee too. Importing a framework registry directly can give the registry and CeeBee's Ant runtime separate CSS-in-JS instances—especially through a workspace or package-manager symlink—and silently extract no rules. A framework adapter creates, extracts, and provides the shared cache through the client entry; cookie and seed helpers remain server-safe:

tsx
'use client';
import { useState } from 'react';import { useServerInsertedHTML } from 'next/navigation';import {  CeebeeAntStyleProvider,  createCeebeeAntStyleCache,  extractCeebeeAntStyles,} from '@ceebee/ui/client';
export function AntStyleRegistry({ children }) {  const [cache] = useState(createCeebeeAntStyleCache);
  useServerInsertedHTML(() => {    const css = extractCeebeeAntStyles(cache);    if (!css) return null;    return <style id="ceebee-ant-cssinjs" dangerouslySetInnerHTML={{ __html: css }} />;  });
  return <CeebeeAntStyleProvider cache={cache}>{children}</CeebeeAntStyleProvider>;}

This adapter is Next-specific only at the insertion hook. Cache ownership, extraction, Ant's runtime, Skin seeds, and hydrated provider stay framework-neutral and versioned together.

Writing a Skin

A Skin rewrites skin tokens only. It never touches structure tokens and never writes a component rule; if a Skin needs a rule, the component is missing a token.

The optional Skins include clarity.css for content-first glass and moodboard.css for the warm, taped-paper direction first shaped for Ceebee List. Load one after styles.css; the component API and accessibility fallbacks remain unchanged. The selector in this page's toolbar previews the same files consumers import.

tsx
import '@ceebee/ui/styles.css';import '@ceebee/ui/skins/clarity.css';// or: import '@ceebee/ui/skins/moodboard.css';

tinted · violet

glass

gradient · blue

Do

Put a project's brand in a Skin file and load it after the base stylesheet.

Don't

Override component classes in the consuming app — that is a token the library is missing, and a fork waiting to happen.

More contrast

prefers-contrast: more steps the borders and the secondary text up without changing hue, so someone who asked their system for stronger contrast gets it from the tokens rather than from a separate theme you have to maintain.

Runtime colour

If a person picks their own accent colour, write the value into the custom property and leave the tokens where they are:

Tokens stay in CSS; JavaScript only supplies a value.