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:
- 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
h5heading, drawn as anh3, 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. - 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. - 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. - A ⌄ (Color) opens Notion’s colors: ten text colors and ten background colors. A text color sets the
colormark, a background thehighlightmark, and Default removes that mark. The palette is exported asTOOLBAR_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.
toolbar?Snippet<[ToolbarController]>
Replaces the toolbar. Rendered while controller.isVisible, centered above the selection.
Snippet<[ToolbarController]>The snippet receives the ToolbarController. Every action keeps and restores the selection:
marks?{ mark, label, icon }[]
The marks that declare a toolbar button, in registration order.
{ mark, label, icon }[]toggleMark(mark)?(mark: string) => void
Toggle a mark over the selection.
(mark: string) => voidisActive(mark)?(mark: string) => boolean
Whether the mark covers every character of the selection, for a pressed state.
(mark: string) => booleankinds?KindRow[]
The kinds the block may turn into while keeping its content.
KindRow[]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).
KindRow | undefinedturnInto(kind)?(kind: KindRow) => void
Convert every block the selection touches, as one undo step.
(kind: KindRow) => voidsetColor(mark, value)?('color' | 'highlight', value: string | null) => void
Set the text or background color; null removes it.
('color' | 'highlight', value: string | null) => voidlinkUrl?string
The selection's current link, or the URL being typed.
stringsetLinkUrl(value)?(value: string) => void
Update linkUrl, for a link field.
(value: string) => voidapplyLink()?() => void
Link the selection to linkUrl; an empty URL removes the link.
() => voidremoveLink()?() => void
Remove the link from the selection.
() => voidpanel?'turn' | 'link' | 'color' | null
The open panel, for your own panels.
'turn' | 'link' | 'color' | nulltogglePanel(panel)?(panel: 'turn' | 'link' | 'color') => void
Open a panel, or close it when it is open.
(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.