Start here

Playground

Try it in the demo →

Rich Text for Canvas has no toolbar of its own: your app keeps its own buttons and menus, in its own design, and each control calls one formatting command. This page wires a small toolbar (font, size, bold, italic, underline, strikethrough, color, bullets, numbers, alignment, undo and redo) to the library, as the demo's Playground does. The pages linked at the end cover each group of commands in full.

What a command changes

Every formatting command works on the current selection:

When The command changes
A box is being edited and words are selected The selected words
A box is being edited and only the cursor shows The cursor's paragraph
A box is selected but not being edited All of the box's text
You pass boxes as the last argument All of the text in those boxes
No box is selected Nothing

Each command is one undo step, even when it changes several boxes; commands on the same box less than a second apart join one step. Commands are on rt.manager on Konva and on rt on a plain canvas, with the same names:

// Konva
const rt = enableRichText(stage);
rt.manager.toggleBold();
rt.manager.toggleBold([headline, footer]); // these boxes, whatever is selected

// Plain canvas
const rt = createCanvasRichText({ onChange: render });
rt.toggleBold();

Build the toolbar

The toolbar is plain HTML in your page:

<div id="toolbar">
  <select id="font"><option>Poppins</option><option>Lora</option><option>Oswald</option></select>
  <select id="size"><option>14</option><option>18</option><option>24</option><option>36</option><option>48</option></select>
  <button id="bold">B</button>
  <button id="italic">I</button>
  <button id="underline">U</button>
  <button id="strike">S</button>
  <input id="color" type="color">
  <button id="bullets">Bullets</button>
  <button id="numbers">Numbers</button>
  <button data-align="left">Left</button>
  <button data-align="center">Center</button>
  <button data-align="right">Right</button>
  <button data-align="justify">Justify</button>
  <button id="undo">Undo</button>
  <button id="redo">Redo</button>
</div>

Connect each control to a command

The same code serves both surfaces: only the first line differs.

const commands = rt.manager; // on a plain canvas: const commands = rt;
const byId = <T extends HTMLElement>(id: string) => document.getElementById(id) as T;

const font = byId<HTMLSelectElement>("font");
font.onchange = async () => {
  await commands.setFontFamily(font.value); // loads the font first, then applies it
  font.blur();
};

const size = byId<HTMLSelectElement>("size");
size.onchange = () => {
  commands.setFontSize(Number(size.value));
  size.blur();
};

byId("bold").onclick = () => commands.toggleBold();
byId("italic").onclick = () => commands.toggleItalic();
byId("underline").onclick = () => commands.toggleUnderline();
byId("strike").onclick = () => commands.toggleStrikethrough();

const color = byId<HTMLInputElement>("color");
color.onchange = () => {
  commands.setColor(color.value);
  color.blur();
};

for (const button of document.querySelectorAll<HTMLButtonElement>("[data-align]")) {
  button.onclick = () => commands.setAlignment(button.dataset.align as "left" | "center" | "right" | "justify");
}

byId("undo").onclick = () => commands.undo();
byId("redo").onclick = () => commands.redo();

setFontFamily returns a promise. The box keeps showing its current font while the new one downloads, and the promise resolves once the text is drawn in the new font. Any Google Fonts family works with no setup; see Fonts for your own.

Keep typing after a toolbar click

While someone types in a box, keys are heard on the window. The library leaves keys alone while the page's focus is in an input, textarea, select or contenteditable element, so typing after choosing from a menu would go to the menu. That is why the menus above call blur() after a change.

Buttons do not take keys, but a click moves the page's focus to the button. To keep focus where it was, prevent the default on mousedown:

const toolbar = byId("toolbar");
toolbar.addEventListener("mousedown", (e) => {
  if ((e.target as HTMLElement).closest("button")) e.preventDefault();
});

A click on your toolbar does not end editing on either surface: editing ends on a press on the canvas outside the box being edited, or with Esc.

Show the current formatting

onFormattingChange reports what the selection looks like every time it changes: the font, size and styles, the color, the alignment, the list, and whether undo and redo can do anything. Your callback is called once right away, and with null when nothing is selected.

import { defaultRichTextContext, type FormattingState } from "@mycreativeshop/rich-text/konva";

let current: FormattingState | null = null;

defaultRichTextContext.onFormattingChange((state) => {
  current = state;
  byId("bold").classList.toggle("active", !!state?.bold);
  byId("italic").classList.toggle("active", !!state?.italic);
  font.value = state?.fontFamily ?? "";
  size.value = state ? String(Math.round(state.fontSize)) : "";
  byId<HTMLButtonElement>("undo").disabled = !state?.canUndo;
  byId<HTMLButtonElement>("redo").disabled = !state?.canRedo;
});
// Plain canvas: rt.onFormattingChange((state) => { ... })

Your own toolbar lists every field of the report and how a full toolbar uses it.

Bullets and numbers

toggleBulletList(style) turns a list on or off for the reached paragraphs, and setBulletStyle(style) changes the marker of paragraphs that are already list items. "disc" is a bullet and "decimal" numbers the items 1. 2. 3. With the report above, one Bullets button and one Numbers button behave as people expect: a second press turns the list off, and pressing the other button switches the marker.

function list(style: "disc" | "decimal") {
  const numbered = current?.listStyle === "decimal";
  if (current?.isList && numbered === (style === "decimal")) commands.toggleBulletList(); // the same button: off
  else if (current?.isList) commands.setBulletStyle(style); // the other button: switch the marker
  else commands.toggleBulletList(style); // not a list yet: on
}

byId("bullets").onclick = () => list("disc");
byId("numbers").onclick = () => list("decimal");

Lists covers every marker, including any character and shapes.

Undo and redo

undo() and redo() step through the same history as Ctrl+Z and Ctrl+Shift+Z (Cmd on a Mac), which work while a box is being edited. Typing, each toolbar command and resizing a box with its handles are steps. To put text changes in your app's own undo history instead, see Your app's undo.

More commands

Page Commands
Character styles setFontFamily, setFontSize, scaleFontSize, setFontWeight, setColor, toggleBold, toggleItalic, toggleUnderline, toggleStrikethrough
Paragraphs setAlignment, setLineHeight, setLetterSpacing, setParagraphSpacing
Lists toggleBulletList, setBulletStyle
Tab stops setTabStops
Your own toolbar onFormattingChange, getFormattingState
Keyboard and mouse copy, cut, paste, undo, redo

In the API reference