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
- RichTextManager: the Konva commands, on
rt.manager - CanvasRichText: the plain-canvas commands, on
rt - FormattingState: what
onFormattingChangereports - defaultRichTextContext