# Headless Toolbar

> Build your own toolbar UI. We handle the state and the commands.

2026-04-10 · Caio Pizzol

Your design system is specific. Your buttons are built. Your icons are chosen. You don't want our toolbar — you want a toolbar that looks like everything else in your app, wired to SuperDoc's editor.

The headless toolbar gives you exactly that. You bring the UI. We give you reactive state and command execution for 38 built-in editor actions across body, header, and footer.

## The problem

Until now, the toolbar state and command logic were tied to SuperDoc's internal UI. If you wanted a custom toolbar, you had to rebuild formatting detection, selection state, command dispatching, and multi-editor context tracking yourself. Teams ended up reverse-engineering our internals or forking the package.

Or — more commonly — they shipped with our default toolbar and a note in the design backlog that said "replace eventually."

## How it works

Don't pass `toolbar` to the SuperDoc constructor. Call `createHeadlessToolbar` with your SuperDoc instance. You get back a controller with three things: a snapshot of current state, a subscription for updates, and an execute function that runs commands.

```ts
import { createHeadlessToolbar } from '@superdoc-dev/superdoc/headless-toolbar';

const toolbar = createHeadlessToolbar({
  superdoc,
  commands: ['bold', 'italic', 'font-size', 'text-align'],
});

// Read current state
const { commands } = toolbar.getSnapshot();
commands.bold?.active;      // true if selection is bold
commands.bold?.disabled;    // true if no editable surface
commands['font-size']?.value; // '12pt'

// Subscribe to changes
toolbar.subscribe(({ snapshot }) => {
  renderToolbar(snapshot);
});

// Run commands
toolbar.execute('bold');
toolbar.execute('font-size', { size: '14pt' });
```

Omit `commands` to track all 38. The snapshot stays in sync as the user types, clicks, or moves between the main body and header/footer surfaces.

## 38 commands, 6 categories


| Category            | Commands                                                                                                  |
| ------------------- | --------------------------------------------------------------------------------------------------------- |
| **Text formatting** | bold, italic, underline, strikethrough, clear-formatting, copy-format                                     |
| **Font**            | font-size, font-family, text-color, highlight-color                                                       |
| **Paragraph**       | text-align, line-height, linked-style, bullet-list, numbered-list, indent-increase, indent-decrease, link |
| **Document**        | undo, redo, ruler, zoom, document-mode                                                                    |
| **Track changes**   | accept-selection, reject-selection                                                                        |
| **Tables**          | insert, add/delete row, add/delete column, merge, split, remove-borders, fix, delete                      |
| **Other**           | image                                                                                                     |


Every command is typed. `execute('font-size', ...)` requires a `{ size }` payload. `execute('text-align', ...)` requires `{ alignment: 'left' | 'center' | 'right' | 'justify' }`. Wrong payload — compile error.

## React and Vue hooks

The controller is framework-agnostic, but we ship hooks that handle subscribe/cleanup for you.

```tsx
import { useHeadlessToolbar } from '@superdoc-dev/superdoc/headless-toolbar/react';

function Toolbar({ superdoc }) {
  const { snapshot, execute } = useHeadlessToolbar(superdoc);
  const bold = snapshot.commands.bold;

  return (
    <button
      onClick={() => execute('bold')}
      aria-pressed={bold?.active}
      disabled={bold?.disabled}
    >
      Bold
    </button>
  );
}
```

Vue composable is the same shape — `snapshot` is a `ShallowRef`, `execute` is a function. Cleanup on unmount is automatic in both.

## Five examples, five frameworks

We shipped reference implementations so you can see how the same API maps to different UI stacks:

- **react-shadcn** — classic top ribbon with Radix primitives
- **react-mui** — floating bubble bar with Material UI
- **vue-vuetify** — sidebar panel with Vuetify 3
- **svelte-shadcn** — compact bottom bar with Svelte 5
- **vanilla** — minimal top bar with zero framework

All five live in `[examples/advanced/headless-toolbar/](https://github.com/superdoc-dev/superdoc/tree/main/examples/advanced/headless-toolbar)` and run in CI.

## Context that follows the editor

Word documents have three editable surfaces: body, headers, and footers. A custom toolbar has to know which one the user is in, because commands dispatch to the active editor.

The snapshot's `context` tells you:

```ts
snapshot.context?.surface;       // 'body' | 'header' | 'footer'
snapshot.context?.isEditable;    // false in view mode
snapshot.context?.selectionEmpty;
```

`execute()` handles the routing — it dispatches to whichever surface is currently focused. You don't manage editor references.

## Get started

```bash
npm install @superdoc-dev/superdoc
```

Read the [headless toolbar docs](https://docs.superdoc.dev/modules/toolbar/headless) or jump straight to the [examples](https://github.com/superdoc-dev/superdoc/tree/main/examples/advanced/headless-toolbar). If you hit a missing command, open an issue — the registry is designed to grow.
