Theming

Kit components read two sets of CSS variables: the Mantine variables of your theme and the kit's own --ae-* tokens from @sinups/ai-kit/styles.css. Without any provider the kit already follows your primary color, fonts and color scheme. AiKitProvideradds the kit's visual language to the stock Mantine components used inside the kit (buttons, inputs, badges, alerts) and exposes a small set of settings.

How theming works

  • --ae-primary points at --mantine-primary-color-filled, --ae-font-* at the Mantine font variables, and kit radii follow theme.defaultRadius. A host that already set primaryColor, defaultRadius and fonts gets a matching kit without configuration.
  • Neutral surfaces (--ae-bg, --ae-fg, --ae-border and the rest) have a light and a dark value and switch with the Mantine color scheme.
  • AiKitProvider applies createAiKitTheme() to its subtree only. Components outside the provider keep the host theme.
  • Inside the kit, filled red buttons use --ae-danger-fill with white text (at least 4.5:1 in both schemes), disabled ghost and light buttons keep their look at reduced opacity, scrollbars appear only on hover or focus, field labels share one xs style and dialogs open with focus inside the body instead of on the close button.

AiKitProvider

Render AiKitProvider inside your MantineProvider, around the kit screens. It needs the host provider: it reads the host theme and color scheme from it.

import "@mantine/core/styles.css";
import "@sinups/ai-kit/styles.css";

import { MantineProvider } from "@mantine/core";
import { AiKitProvider, McpSettingsPanel } from "@sinups/ai-kit";

export function App() {
  return (
    <MantineProvider theme={appTheme}>
      <AppHeader />
      <AiKitProvider accent="violet" radius="default" density="compact">
        <McpSettingsPanel servers={servers} />
      </AiKitProvider>
    </MantineProvider>
  );
}

Mantine writes CSS variables only at the document root. The provider therefore renders a scoped stylesheet with the variables that differ between the host theme and the kit theme, and adds its scope class to portals, so menus, modals and popovers opened from kit components look the same as the inline content. See the AiKitProvider reference for the full prop list.

Settings

Every setting is optional. An unset value keeps the host value.

Prop
Values
Effect
accent
gray, blue, indigo, violet, grape, pink
Sets primaryColor and a primaryShade per scheme. Each accent is tested for 4.5:1 text contrast on its fill in both schemes.
radius
sharp, default, round
Sets defaultRadius to xs, md or lg. Composer, card, notice, row and button radii scale together.
density
default, compact
default: 20, 24 and 28px controls and 28px rows. compact: 20, 22 and 24px controls and 24px rows.
colorScheme
light, dark, auto
Calls setColorScheme of the host provider, so it changes the color scheme of the whole app, not only the subtree.
persistKey
string
Saves changes made through useAiKitTheme or the customizer to localStorage under this key and restores them. Invalid stored values are ignored.

Customizer panel

AiKitThemeCustomizeris a ready settings panel with Color, Radius, Density and Mode sections. Inside a provider it edits that provider's settings. sections hides sections, for example Mode when your app controls the color scheme; accents limits the offered colors; labels translates the text.

import { AiKitProvider, AiKitThemeCustomizer } from "@sinups/ai-kit";

export function Settings() {
  return (
    <AiKitProvider persistKey="workspace-kit-theme">
      <AiKitThemeCustomizer sections={{ mode: false }} />
    </AiKitProvider>
  );
}

To store the settings yourself, control the panel with value and onChange and pass the same object to the provider. defaultValue makes it uncontrolled without a provider.

import { useState } from "react";
import {
  AiKitProvider,
  AiKitThemeCustomizer,
  type AiKitThemeSettings,
} from "@sinups/ai-kit";

export function ThemeSettings() {
  const [settings, setSettings] = useState<AiKitThemeSettings>({ accent: "indigo" });

  return (
    <>
      <AiKitThemeCustomizer
        value={settings}
        onChange={(next) => {
          setSettings(next);
          saveUserPreference(next);
        }}
        accents={["gray", "indigo", "violet"]}
      />
      <AiKitProvider {...settings}>
        <ChatScreen />
      </AiKitProvider>
    </>
  );
}

Reading and changing settings

useAiKitTheme() returns settings (props plus changes), defaults (props only), setSettings(patch), reset(), hostTheme and aiKit, the resolved theme.other.aiKit values: radii, controlHeights, density, contextPadding and userMessagePadding. It throws outside a provider; useOptionalAiKitTheme() returns null there instead.

import { Button } from "@mantine/core";
import { useAiKitTheme } from "@sinups/ai-kit";

export function CompactToggle() {
  const { settings, setSettings, reset, aiKit } = useAiKitTheme();

  return (
    <>
      <Button
        onClick={() =>
          setSettings({ density: settings.density === "compact" ? "default" : "compact" })
        }
      >
        Row height: {aiKit.controlHeights.row}
      </Button>
      <Button variant="subtle" onClick={reset}>
        Reset
      </Button>
    </>
  );
}

Theme and token overrides

theme is a regular MantineThemeOverride merged on top of the kit theme. tokens sets --ae-* values for the subtree; keys are token names without the --ae- prefix, typed as AeTokenOverrides, and win over the settings. The full list of names is exported as AE_TOKENS.

<AiKitProvider
  accent="blue"
  theme={{
    fontFamily: "Inter, sans-serif",
    components: { Button: { defaultProps: { size: "sm" } } },
  }}
  tokens={{
    "tool-radius": "12px",
    "user-message-bg": "var(--mantine-color-blue-light)",
  }}
>
  <AgentChat {...chat} />
</AiKitProvider>

Tokens are ordinary CSS custom properties, so a stylesheet can also set them on any ancestor of a kit component:

.support-chat {
  --ae-max-width: 720px;
  --ae-user-message-bg: var(--mantine-color-gray-1);
  --ae-tool-radius: 6px;
}

Nesting in a host app

  • Put AiKitProvider below MantineProvider, never above it. Several providers with different settings can live on one page.
  • Host UI rendered inside a kit subtree, for example your own toolbar between kit panels, goes into AiKitHostScope. It restores the host theme and host CSS variables for its children.
  • If your stylesheet uses CSS layers, import @sinups/ai-kit/styles.layer.css instead of styles.css: it wraps the kit styles in @layer mantine.
<AiKitProvider accent="grape">
  <MasterDetail
    list={<AgentList agents={agents} onSelect={select} />}
    detail={
      <>
        <AiKitHostScope>
          <HostToolbar />
        </AiKitHostScope>
        <AgentDetail agent={selected} />
      </>
    }
  />
</AiKitProvider>

Global theme

When the whole app should use the kit language, merge the kit theme into your theme instead of wrapping screens. mergeAiKitTheme(appTheme) keeps your values and fills the rest from the kit. Add the ae-kit class (AI_KIT_SCOPE_CLASS) to the app root; portals receive it from the theme. createAiKitTheme(overrides) returns the kit theme with your overrides on top, when you need the kit values to win.

import { MantineProvider } from "@mantine/core";
import { AI_KIT_SCOPE_CLASS, mergeAiKitTheme } from "@sinups/ai-kit";

export function App() {
  return (
    <MantineProvider theme={mergeAiKitTheme(appTheme)}>
      <div className={AI_KIT_SCOPE_CLASS}>
        <Routes />
      </div>
    </MantineProvider>
  );
}

Opting out

A stock component inside the kit subtree keeps its own className, classNames and vars; they add to the kit styles. To render it with plain Mantine styles, pass unstyled, variant="unstyled" or data-ai-kit-unstyled.

<AiKitProvider>
  <Button unstyled>Keeps Mantine defaults</Button>
  <Button data-ai-kit-unstyled>Also opted out</Button>
</AiKitProvider>