[]previous and next page

Customizing the UI

Turn parts of the default interface off, swap the toolbar's tools, add your own buttons and menu items, and restyle it.

The default UI (canvasflow/ui) is written without a framework, so it works the same in plain JS, React, Vue, Svelte or anything else. It's built to be trimmed: every area switches off, and the slots take your own elements.

Mount it#

ts
import { createCanvasFlow } from 'canvasflow/dom'
import { mountUI } from 'canvasflow/ui'

const app = createCanvasFlow(document.getElementById('board')!)
const ui = mountUI(app, {
  minimap: false,
  pages: false,
})

In a binding, pass the same object as the ui prop (or option): <CanvasFlow ui={{ minimap: false }} />.

What's in it#

Area Option What it is
Toolbar toolbar The tools, the tool lock and the "more tools" menu
Style panel stylePanel Colors, fill, stroke, sloppiness, line type and ends, fonts, opacity, layer and alignment actions
Main menu menu Open, save, export, preferences, help
Pages pages The page switcher
Zoom zoom Zoom in, out, reset and undo/redo
Layers layers The layers panel
Minimap minimap An overview of the page
Empty hint emptyHint The hand-written hint on an empty canvas

The ⌘K command palette, the context menu, the shortcuts sheet and toasts are always there; they appear only when asked for.

Change the tools#

ts
mountUI(app, {
  tools: [
    { id: 'select', icon: 'pointer', label: 'Select' },
    { id: 'draw', icon: 'pencil', label: 'Draw' },
    { id: 'arrow', icon: 'arrow', label: 'Arrow' },
    { id: 'note', icon: 'note', label: 'Sticky note' },
  ],
  moreTools: [],
})

A toolbar entry is just a tool id, an icon name and a label. Your own tools (see Custom tools) go in the same way.

Add your own controls#

Put elements in the top corners, add entries to the main menu, or mount a component in any slot:

ts
const share = document.createElement('button')
share.textContent = 'Share'
share.className = 'cfu-btn'
share.onclick = () => openShareDialog()

const ui = mountUI(app, {
  topRight: [share],
  menuItems: () => [{ label: 'Back to boards', icon: 'folder', onSelect: () => (location.href = '/') }],
})

The slots are top-left, top-center, top-right, bottom-left, bottom-center, bottom-right, left and right. A component added with ui.add(component, slot) gets an update() call whenever the editor changes, so it can stay in sync without subscribing itself.

Restyle it#

The UI reads CSS custom properties, so a few lines restyle everything without fighting specificity:

board.csscss
.cfu-root {
  --cfu-radius: 14px; /* panels and popovers */
  --cfu-btn: 38px; /* button size */
  --cfu-panel: rgba(255, 252, 247, 0.96);
  font-family: 'Inter', system-ui, sans-serif;
}

.cfu-root[data-theme='dark'] {
  --cfu-panel: rgba(28, 24, 20, 0.94);
}

The canvas accent (selection, handles, active tool) comes from the theme, so set it there and the UI follows.

Toasts#

The UI shows the editor's toast events (with an Undo button when the change can be undone). Show your own through the same toaster so they look and behave the same:

ts
ui.toaster.show({ message: 'Board duplicated', kind: 'success' })

When to stop customizing#

If you're turning off most areas and restyling the rest, you'll be happier building your own interface on the editor. It's less work than it sounds: Building your own UI.

Right to left#

The interface mirrors itself when the layout direction is right to left: panels swap sides, menus open from the start edge, directional icons flip. Set it with the dir option of createCanvasFlow (or the prop of a binding), or pick it in the menu under Preferences → Layout direction. Your own elements in the slots get the same dir through inheritance, so use logical CSS properties (margin-inline-start, inset-inline-end) in them. See Right to left.