The right side of the header — search, the appearance menu, language switcher, version switcher, GitHub link, and the Design Token / AI Chat triggers — is rendered from a single array: the `headerRightItems` field. Each entry is a discriminated-union item rendered in order.

[Dynamic content unavailable in text export]

This complements [Header Navigation](https://zudo-doc.takazudomodular.com/docs/guides/header-navigation), which configures the **left-side tabs**. Both are configured in `zfb.config.ts` and are independent of each other.



## Default configuration

```ts title="zfb.config.ts"
export default defineConfig(
  zudoDoc({
    // ...
    githubUrl: "https://github.com/zudolab/zudo-doc",
    headerRightItems: [
      { type: "trigger", trigger: "design-token-panel" },
      { type: "trigger", trigger: "ai-chat" },
      { type: "component", component: "version-switcher" },
      { type: "component", component: "github-link" },
      { type: "component", component: "theme-toggle" },
      { type: "component", component: "search" },
      { type: "component", component: "language-switcher" },
    ],
  }),
);
```

## Item types

### `component`

References a built-in UI piece. Most are gated by their own feature flag; `search` is ungated.

| Component | Feature flag |
|---|---|
| `theme-toggle` | `colorMode` |
| `language-switcher` | `locales` (needs at least one non-default locale) |
| `version-switcher` | `versions` |
| `github-link` | `githubUrl` |
| `search` | none — always available |

```ts
{ type: "component", component: "github-link" }
```

When the associated feature is disabled the entry is skipped — you can leave it in the array unconditionally. `search` has no dedicated feature flag: the renderer is always available, and its presence and position are controlled purely by where (or whether) it appears in the array.

### `trigger`

Opens a built-in modal or panel.

| Trigger | Feature flag |
|---|---|
| `design-token-panel` | `designTokenPanel` |
| `ai-chat` | `aiAssistant` |

```ts
{ type: "trigger", trigger: "ai-chat" }
```

The `design-token-panel` trigger button dispatches the `toggle-design-token-panel` custom event on `window`. The zdtp panel (`@takazudo/zdtp`) listens for this event natively — no additional wiring is needed.

### `link`

A custom external or internal link, rendered with an optional icon.

```ts
{
  type: "link",
  href: "https://discord.example",
  ariaLabel: "Discord",
  icon: "github", // only "github" is bundled today
}
```

External URLs open in a new tab automatically.

### `html`

Escape hatch for injecting arbitrary markup.

```ts
{ type: "html", html: '<span class="text-caption">beta</span>' }
```

Use sparingly — the html string is rendered verbatim.

## Customisation recipes

### Point the GitHub icon at your repo

Set `githubUrl` at the top level of `zudoDoc({...})` in `zfb.config.ts`. The `github-link` component reads that value, so you don't duplicate the URL inside `headerRightItems`.

```ts title="zfb.config.ts"
export default defineConfig(
  zudoDoc({
    githubUrl: "https://github.com/your-org/your-repo",
  }),
);
```

Setting `githubUrl: false` removes the icon without having to edit the array.

### Drop an item

Remove it from the array — don't rely on the feature flag to hide it. For example, to hide the AI chat trigger:

```ts
headerRightItems: [
  { type: "trigger", trigger: "design-token-panel" },
  // { type: "trigger", trigger: "ai-chat" },  // removed
  { type: "component", component: "version-switcher" },
  { type: "component", component: "github-link" },
  { type: "component", component: "theme-toggle" },
  { type: "component", component: "language-switcher" },
],
```

### Add a custom link

```ts
headerRightItems: [
  // ...existing items
  {
    type: "link",
    href: "https://discord.gg/your-server",
    ariaLabel: "Discord",
    icon: "github", // until more icons are bundled
  },
],
```

### Add a custom renderer

Project-owned component names are resolved through `chromeBindings.headerRightComponents`; see [Host Chrome Bindings](https://zudo-doc.takazudomodular.com/docs/reference/host-chrome-bindings) for the complete typed setup. Treat a renderer as SSR-presentational unless its client island also has a supported static registration path.

For a registered island whose props belong to the browser session rather than the current page, add `data-zd-props-preserve` to the live island or an ancestor inside the persisted header. The live DOM governs this opt-out: navigation neither replaces that island's serialized props nor marks it for remount. Putting the attribute on the persisted header opts out every nested island, so leave it off page-derived islands that should receive the incoming page's props.

## Customizing the appearance menu

The built-in `theme-toggle` serves the standard header. To place the same appearance menu elsewhere, import `ThemeToggle` from the public `@takazudo/zudo-doc/theme-toggle` subpath and compose it through a fixed-target server boundary.

```tsx
import { ThemeToggle } from "@takazudo/zudo-doc/theme-toggle";
```

The exported `ThemeToggle` renders its own compact `<button>` and a Light / Dark / System menu. Its sun, moon, or monitor icon shows the selected preference; when System is selected, it remains a monitor even as the device changes the page's resolved light/dark appearance. `defaultMode` remains a light/dark fallback, and `respectPrefersColorScheme` controls whether System is the first-visit choice. Use its `labels` prop to provide localized menu and accessibility text. You can place it anywhere and compose UI around it, but its button markup and icons are fixed — importing it reuses the package appearance menu (and avoids the collision below), rather than restyling it. If you need different menu markup or icons, build your own component and give it a name **other than** `ThemeToggle`, so it doesn't claim the package's island marker.

### Why import the package island

Each client entry must have one unique island identity. A second source file claiming `ThemeToggle` is an error, not a benign warning or a supported scan-order override. Reuse the package entry; if you build a different menu, give its client entry a distinct exported name and statically import that one target into its server boundary. Do not rely on `displayName` assignments or duplicate local shims to replace a package island.

This repository validates entry graphs with `node scripts/check-client-export-names.mjs`. That gate rejects duplicate identities before a build. A callable renderer in the virtual bindings map alone does not make its client entry scanner-reachable; follow the concrete static route import in [Custom Components](https://zudo-doc.takazudomodular.com/docs/guides/custom-components#interactive-islands-experimental).

[Dynamic content unavailable in text export]

Import the appearance control from the public `@takazudo/zudo-doc/theme-toggle` subpath. It is a zudo-react client component; mount it through a statically imported server boundary using `Island`.



[Dynamic content unavailable in text export]

The package's `applyColorScheme` (in `theme-toggle/color-scheme-sync.ts`) deliberately does **not** clear design-token panel state when the resolved color scheme changes — `@takazudo/zdtp` owns its storage lifecycle. Selecting System while the device already matches the current appearance changes the saved preference without rebuilding the panel; a later device appearance change updates the active mode. Palette, Spacing, Font, and Size tweaks survive appearance changes untouched, and the Color tab's ref/literal defaults re-seed to the active mode. Saved Color overrides are scheme- and mode-scoped: a mapping edited in light does not replace dark's default or saved mapping, and each mode restores its own saved choice when you return to it. The package-default builder declares separate `Default Light` and `Default Dark` identities in `panelSettings.colorMode`. See [Mode-scoped editing model](https://zudo-doc.takazudomodular.com/docs/reference/design-token-panel#the-tab-strip-color-mode-scoped-editing-model) for the full semantics.



## Ordering

Items render in the order they appear in the array. The cluster is right-aligned, so the first entry sits closest to the nav and the last entry sits closest to the viewport edge.