Skip to content
Edytor
Esc
↑↓navigate↵open⌘Jpreview
On this page

Toolbar

The toolbar plugin shows Notion's floating toolbar over text selections, with Turn into, a link panel, a button for every mark that declares one, and text and background colors.

toolbarPlugin shows Notion’s formatting toolbar above a text selection: [Kind ⌄] | Link | B I U S </> | A ⌄. The mark buttons come from the mark definitions: every mark with a toolbar entry gets one.

<script lang="ts">
	import { Edytor, toolbarPlugin } from 'edytor';
</script>

<Edytor plugins={[toolbarPlugin]} />

createToolbarPlugin({ toolbar }) builds one that renders your markup (see Custom markup). It formats through richTextOperations, so it relies on the rich text plugin (on by default), or on your own plugin that defines the link, color and highlight marks if you want the link and color panels to work.

What it shows

From left to right:

  1. The kind button, labeled with the block’s kind (“Heading 2”, “Text”…): the preset sharing the most data with the block, or drawn with the same element (a stored h5 heading, drawn as an h3, is “Heading 3”). It opens a “Turn into” panel listing the kinds that keep content, with ✓ on the current one. Picking one converts every block the selection touches, nested ones included, as one undo step, and keeps the selection.
  2. Link opens a link panel: a URL field (data-testid="toolbar-link-input") showing the selection’s current link, with Apply and Remove. Enter in the field applies and closes the panel. Applying an empty field removes the link.
  3. One button per mark whose definition has toolbar: { label, icon }, in registration order. With the rich text plugin: bold, italic, underline, strike and code. A button toggles its mark over the selection, and shows pressed (aria-pressed="true", in blue) when the mark covers the whole selection.
  4. A ⌄ (Color) opens Notion’s colors: ten text colors and ten background colors. A text color sets the color mark, a background the highlight mark, and Default removes that mark. The palette is exported as TOOLBAR_COLORS ({ name, text, background }).

One panel is open at a time; clicking its button again closes it. Panels drop below the bar, over the text.

Color Text Background
Default none none
Gray #7d7a75 #f0efed
Brown #9f765a #f5ede9
Orange #d27b2d #fbebde
Yellow #cb9434 #f9f3dc
Green #50946e #e8f1ec
Blue #387dc9 #e5f2fc
Purple #9a6bb4 #f3ebf9
Pink #c14c8a #fae9f1
Red #cf5148 #fce9e7

The values are stored in the marks as written, so documents keep them if you restyle the editor.

Add a button for your own mark by declaring toolbar on it:

marks: {
	keyboard: { tag: 'kbd', toolbar: { label: 'Keyboard', icon: '⌨' } }
}

The icon is text: a letter, a symbol or an emoji.

When it shows

The toolbar shows when all of these hold:

  • the editor is not readonly (turning it readonly hides the toolbar; it shows again over the same selection when the editor is editable);
  • the selection is a non-collapsed text range;
  • no whole block and no inline atom is selected;
  • the selection is not inside a void or an island block (such as a code block).

It follows the selection as it changes, and repositions on scroll, resize and edits. Buttons keep the selection: clicking one does not move the caret, and the selection is restored after the format is applied.

A URL typed in the link panel goes through the same sanitization as every link: a javascript: or other unsafe scheme is ignored. Colors go through setMarkValueAtRange and removeMarkAtRange (see Formatting from code).

Placement and styling

The toolbar mounts in the editor’s overlay, in a position: fixed host with the attribute data-edytor-toolbar-host and z-index: 60. It is centered above the selection, or placed below it when there is no room above, and kept inside the viewport.

Its markup and styles are built in, after Notion’s. The bar has data-edytor-toolbar-bar and data-testid="selection-toolbar", the Link button toolbar-link, the link panel’s buttons toolbar-link-apply and toolbar-link-remove, and each mark button the class mark-<name> and a data-testid of toolbar-<name>.

Custom markup

createToolbarPlugin takes a toolbar snippet that replaces the whole bar. The plugin keeps deciding when it shows (the rules above), follows the selection and places it.

PropType
toolbar?Snippet<[ToolbarController]>

Replaces the toolbar. Rendered while controller.isVisible, centered above the selection.

TypeSnippet<[ToolbarController]>

The snippet receives the ToolbarController. Every action keeps and restores the selection:

PropType
marks?{ mark, label, icon }[]

The marks that declare a toolbar button, in registration order.

Type{ mark, label, icon }[]
toggleMark(mark)?(mark: string) => void

Toggle a mark over the selection.

Type(mark: string) => void
isActive(mark)?(mark: string) => boolean

Whether the mark covers every character of the selection, for a pressed state.

Type(mark: string) => boolean
kinds?KindRow[]

The kinds the block may turn into while keeping its content.

TypeKindRow[]
currentKind?KindRow | undefined

The row naming the selection's block: of its kind's rows, the one whose preset data shares the most values with the block's (the first on a tie).

TypeKindRow | undefined
turnInto(kind)?(kind: KindRow) => void

Convert every block the selection touches, as one undo step.

Type(kind: KindRow) => void
setColor(mark, value)?('color' | 'highlight', value: string | null) => void

Set the text or background color; null removes it.

Type('color' | 'highlight', value: string | null) => void
linkUrl?string

The selection's current link, or the URL being typed.

Typestring
setLinkUrl(value)?(value: string) => void

Update linkUrl, for a link field.

Type(value: string) => void
applyLink()?() => void

Link the selection to linkUrl; an empty URL removes the link.

Type() => void
removeLink()?() => void

Remove the link from the selection.

Type() => void
panel?'turn' | 'link' | 'color' | null

The open panel, for your own panels.

Type'turn' | 'link' | 'color' | null
togglePanel(panel)?(panel: 'turn' | 'link' | 'color') => void

Open a panel, or close it when it is open.

Type(panel: 'turn' | 'link' | 'color') => void
<script lang="ts">
	import { Edytor, createToolbarPlugin, type ToolbarController } from 'edytor';

	const plugins = [createToolbarPlugin({ toolbar })];
	const keep = (event: MouseEvent) => event.preventDefault();
</script>

{#snippet toolbar(controller: ToolbarController)}
	<div class="bar" data-edytor-toolbar-bar>
		{#each controller.marks as { mark, label, icon } (mark)}
			<button title={label} onmousedown={keep} onclick={() => controller.toggleMark(mark)}>{icon}</button>
		{/each}
		<button onmousedown={keep} onclick={() => controller.setColor('highlight', '#fbf3db')}>Highlight</button>
	</div>
{/snippet}

<Edytor {plugins} />

Call preventDefault on mousedown in every button, or the click moves the selection before the action runs. The plugin places the host by the element marked data-edytor-toolbar-bar (else your snippet’s first element), so a panel that opens below the bar does not push the bar up. TOOLBAR_COLORS holds Notion’s palette for your own color panel. See Menus and handles for the other menus.

Was this page helpful?