Headless Toolbar

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

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.

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.

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:

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

npm install @superdoc-dev/superdoc

Read the headless toolbar docs or jump straight to the examples. If you hit a missing command, open an issue — the registry is designed to grow.