Customization

Customization

Cmd+kit is configured in code. The main customization surface is the command structure itself, followed by messages, theme tokens, render overrides, and optional async sources.

If you want a guided UI for exploring these settings before coding them, see the playground documentation.

Define sections and items

const sections = [
  {
    id: "navigation",
    title: "Navigation",
    items: [
      {
        id: "dashboard",
        title: "Dashboard",
        subtitle: "Open the main workspace",
        href: "/dashboard",
        keywords: ["home", "workspace"]
      }
    ]
  }
];

Nested navigation

const sections = [
  {
    id: "search",
    title: "Search",
    items: [
      {
        id: "docs",
        title: "Documentation",
        children: [
          {
            id: "guides",
            title: "Guides",
            items: [
              { id: "api", title: "API reference" }
            ]
          }
        ]
      }
    ]
  }
];

Theme tokens

<CommandPalette
  sections={sections}
  theme={{
    light: {
      accentColor: "#0fa6d8",
      backgroundColor: "#ffffff",
      textColor: "#0e1720"
    },
    dark: {
      accentColor: "#12b5e5",
      backgroundColor: "#0f1720",
      textColor: "#f5fbff",
      mutedColor: "#9fb4c4",
      borderColor: "#264152",
      overlayColor: "rgba(4, 9, 13, 0.64)",
      radius: "22px",
      shadow: "0 24px 80px rgba(0, 0, 0, 0.42)"
    }
  }}
/>

Icons

Icons are usually customized in the render layer. In React and Preact use renderItem or renderers.item. In Vue use the item slot.

renderItem={(item, active) => (
  <div className={active ? "palette-row is-active" : "palette-row"}>
    <MyIcon name={item.id} />
    <span>{item.title}</span>
  </div>
)}
<template #item="{ item, active }">
  <div :class="['palette-row', { 'is-active': active }]">
    <MyIcon :name="item.id" />
    <span>{{ item.title }}</span>
  </div>
</template>

Messages

<CommandPalette
  sections={sections}
  messages={{
    searchPlaceholder: "Search commands",
    noResults: "No results match your query.",
    closeLabel: "Close palette"
  }}
/>

Render and style overrides

<CommandPalette
  classNames={{
    dialog: "palette-shell",
    item: "palette-item",
    emptyState: "palette-empty"
  }}
  renderers={{
    title: ({ activeTitle, breadcrumbs }) => (
      <span>{breadcrumbs.join(" / ") || activeTitle}</span>
    ),
    emptyState: ({ query }) => <span>No result for "{query}"</span>
  }}
  sections={sections}
/>

Async source

<CommandPalette
  source={async () => {
    const response = await fetch("/api/commands");
    return response.json();
  }}
  title="Workspace commands"
/>

Generate CSS variables

import { createThemeCssText } from "@cmd-kit/core";

const themes = {
  light: { accentColor: "#0fa6d8", backgroundColor: "#ffffff" },
  dark: { accentColor: "#12b5e5", backgroundColor: "#0f1720" }
};

const darkCss = createThemeCssText(themes.dark);
const lightCss = createThemeCssText(themes.light);

const themeBlock = `:root {
${darkCss}
}

html[data-theme="light"] {
${lightCss}
}`;

FAQ

Should icon components live in the command data model?

Usually no. Keep data framework-agnostic and map icons in renderers/slots.

What is the cleanest styling strategy?

Use theme for shared design tokens and classNames for slot-level CSS control.

Can I use both nested navigation and async source data?

Yes, as long as async payloads keep the same section/item shape including nested children.

How should I localize placeholder and empty-state text?

Override messages from your app-level i18n layer instead of hard-coding strings in command data.

How do I keep custom renderers maintainable?

Keep renderer functions focused on presentation and avoid embedding business logic that belongs in command generation.