@use-kona/editorv0.2.2
Editor demo
BasicFormattingPlugin
Adds bold, italic, underline, and strikethrough formatting.
How to use
Select text in the editor and choose Bold, Italic, Underline, or Strikethrough from the floating menu. Try making “Easy to extend” bold.
How it works
- Text marks
- The plugin renders bold, italic, underline, and strikethrough marks. It also handles Cmd or Ctrl with B, I, and U. The floating buttons in this demo are supplied by the app.
- toggleBold(editor)
- Call a toggle with the current Slate editor to apply or remove that mark. Use isMarkActive(editor, "bold") to display the button’s pressed state.
Setup example
Add the plugin instance to the plugins array you pass to KonaEditor.
import { BasicFormattingPlugin } from '@use-kona/editor';
import type { Editor } from 'slate';
const formatting = new BasicFormattingPlugin();
function toggleBold(editor: Editor) {
BasicFormattingPlugin.toggleBold(editor);
}ListsPlugin
Organizes content into bulleted and numbered lists.
How to use
Put the cursor in a paragraph and choose Bulleted list or Numbered list from the toolbar. Press Enter to add the next item.
How it works
- toggleList(editor, type)
- Pass BULLETED_LIST_ELEMENT or NUMBERED_LIST_ELEMENT to turn the current block into a list item or switch an existing list to that type.
- Toolbar controls
- The plugin renders and edits list structures. The app adds the buttons and can use isListActive(editor, type) to show which list type is active.
Setup example
Add the plugin instance to the plugins array you pass to KonaEditor.
import { ListsPlugin } from '@use-kona/editor';
import type { Editor } from 'slate';
const lists = new ListsPlugin();
function makeBulletedList(editor: Editor) {
ListsPlugin.toggleList(editor, ListsPlugin.BULLETED_LIST_ELEMENT);
}LinksPlugin
Inserts links and lets you edit or open them.
How to use
Select a phrase, choose Add link in the floating menu, and enter a URL. Click the existing “Kona application” link to see the Edit and Open actions.
How it works
- renderHint({ getUrl, getLinkElement, getEditor })
- With the default link renderer, this callback supplies the contents of the popup that opens when you click a link. The getters expose the URL, Slate link node, and editor.
- Adding and editing links
- Call LinksPlugin.addLink(editor, url) with the current selection to add or update a link. The demo replaces the default link renderer with its own link chip and Edit/Open popup.
Setup example
Add the plugin instance to the plugins array you pass to KonaEditor.
import { LinksPlugin } from '@use-kona/editor';
const links = new LinksPlugin({
renderHint: ({ getUrl }) => (
<a
href={getUrl()}
target="_blank"
rel="noopener noreferrer"
onMouseUp={(event) => event.stopPropagation()}
>
Open link
</a>
),
});CodeBlockPlugin
Adds code blocks with language selection and syntax highlighting.
How to use
Choose Code block in the toolbar, pick a language from the block’s dropdown, and type a line of code such as console.log("Hello").
How it works
- renderCodeBlock(language, setLanguage, { Content, element })
- Render your controls around Content, which is the plugin’s editable code and syntax highlighting. Use the language string as the picker’s value and call setLanguage to update that block.
- element
- The current Slate code block contains the code lines. A Copy button can call element.children.map(Node.string) using Node from Slate, then join the lines with newline characters.
Setup example
Add the plugin instance to the plugins array you pass to KonaEditor.
import { CodeBlockPlugin } from '@use-kona/editor';
const codeBlocks = new CodeBlockPlugin({
renderCodeBlock: (language, setLanguage, { Content }) => (
<div>
<div contentEditable={false}>
<select
aria-label="Code language"
value={language || 'plaintext'}
onChange={(event) => setLanguage(event.target.value)}
>
<option value="plaintext">Plain text</option>
<option value="javascript">JavaScript</option>
<option value="typescript">TypeScript</option>
</select>
</div>
<Content />
</div>
),
});EmojiPlugin
Searches for and inserts emoji as you type.
How to use
Type a colon followed by a search term, such as :rocket. Choose 🚀 from the suggestions to insert it.
How it works
- onSearch(query)
- The plugin sends the text after the colon without the colon itself. Your app owns the emoji catalog and filters it using this query. This example uses two Unicode emoji.
- renderMenu({ insertEmoji })
- Render the matching choices inside the popup. Call insertEmoji(id, query, editor) to replace the colon and query with an emoji node. Keep the Slate selection in place while the user picks a result.
- renderEmoji(id)
- Return the display for the ID saved in the node. The demo uses Emoji Mart IDs and a custom renderer. This example stores and renders the Unicode character directly.
Setup example
Add the plugin instance to the plugins array you pass to KonaEditor.
import { EmojiPlugin } from '@use-kona/editor';
import { useStore } from '@nanostores/react';
import { atom } from 'nanostores';
import { useSlateStatic } from 'slate-react';
const query = atom('');
const choices = [
{ id: '🚀', name: 'rocket' },
{ id: '🎉', name: 'party' },
];
const emoji = new EmojiPlugin({
onSearch: (text) => query.set(text),
renderMenu: ({ insertEmoji }) => <EmojiChoices insert={insertEmoji} />,
renderEmoji: (id) => id,
});
function EmojiChoices({ insert }: {
insert: (
id: string,
query: string,
editor: ReturnType<typeof useSlateStatic>,
) => void;
}) {
const text = useStore(query);
const editor = useSlateStatic();
return choices
.filter(({ name }) => name.includes(text.toLowerCase()))
.map(({ id, name }) => (
<button
key={id}
type="button"
aria-label={name}
onMouseDown={(event) => event.preventDefault()}
onClick={() => insert(id, text, editor)}
>
{id}
</button>
));
}PlaceholderPlugin
Shows a writing prompt when a block is empty.
How to use
Place the cursor in the empty paragraph at the bottom of the editor. The “Write / to open command menu” prompt disappears when you start typing.
How it works
- focused / unfocused
- Provide the prompt to use while the editor has focus and when it does not. The unfocused prompt can appear in an empty document. The demo uses the same text for both.
- ignoreNodes
- Pass block type strings to suppress prompts inside those blocks. A prompt that mentions slash commands assumes CommandsPlugin is also registered.
Setup example
Add the plugin instance to the plugins array you pass to KonaEditor.
import {
CodeBlockPlugin,
PlaceholderPlugin,
} from '@use-kona/editor';
const placeholders = new PlaceholderPlugin({
focused: 'Start writing…',
unfocused: 'Write a note',
ignoreNodes: [
CodeBlockPlugin.CODE_ELEMENT,
CodeBlockPlugin.CODE_LINE_ELEMENT,
],
});NodeIdPlugin
Gives each block a unique ID to track it as content changes.
How to use
Pass an ID generator when configuring the plugin. The editor assigns a nodeId to new blocks, which you can read from the document value in onChange.
How it works
- generateId()
- Return a new unique string on each call. The plugin writes nodeId on Slate elements, including newly inserted and split blocks. Read the field from the editor value in onChange.
- ID lifetime
- Existing IDs are reused when content is loaded. The plugin can generate a new ID on structural changes such as converting a paragraph to a heading.
Setup example
Add the plugin instance to the plugins array you pass to KonaEditor.
import { NodeIdPlugin } from '@use-kona/editor';
const nodeIds = new NodeIdPlugin({
generateId: () => crypto.randomUUID(),
});
// A paragraph in the editor value can look like this.
const paragraph = {
type: 'paragraph',
nodeId: 'e07d32fa-360e-413e-823b-bab97a103c31',
children: [{ text: 'Project notes' }],
};HeadingsPlugin
Structures a document with three levels of headings.
How to use
Place the cursor on a line and choose Heading 1, Heading 2, or Heading 3 in the toolbar. Try turning “Project notes” into a Heading 2.
How it works
- HeadingLevel1 / HeadingLevel2 / HeadingLevel3
- These constants are the Slate block types h1, h2, and h3. Use them in your document data and when configuring the outline, collapsible sections, or shortcuts.
- toggleHeading2(editor)
- Converts the current block to a level-two heading. Calling it on an active level-two heading turns that block back into a paragraph.
Setup example
Add the plugin instance to the plugins array you pass to KonaEditor.
import { HeadingsPlugin } from '@use-kona/editor';
import type { Editor } from 'slate';
const headings = new HeadingsPlugin();
function toggleHeading2(editor: Editor) {
HeadingsPlugin.toggleHeading2(editor);
}CollapsibleBlocksPlugin
Collapses and expands the content beneath a heading.
How to use
Use the chevron beside the “Usage” heading to hide the blocks underneath it. Click the chevron again to expand the section.
How it works
- Heading types
- Pass registered block types in order from highest to lowest level. Register HeadingsPlugin as well when you use its heading types.
- Section boundaries
- The chevron on a top-level heading hides the following blocks up to the next heading of the same or higher level. The heading node stores the collapsed flag.
Setup example
Add the plugin instance to the plugins array you pass to KonaEditor.
import {
CollapsibleBlocksPlugin,
HeadingsPlugin,
} from '@use-kona/editor';
const sections = new CollapsibleBlocksPlugin([
HeadingsPlugin.HeadingLevel1,
HeadingsPlugin.HeadingLevel2,
HeadingsPlugin.HeadingLevel3,
]);ShortcutsPlugin
Turns Markdown-style shortcuts into formatted text and blocks.
How to use
At the start of an empty line, type ## followed by Space to make a level-two heading. Type **bold** followed by Space to make the word bold.
How it works
- getShortcuts() / shortcuts
- getShortcuts is the demo’s own function that returns an array of rules for headings, lists, code blocks, and inline formatting. You can pass the array directly. Each rule pairs a trigger character with a text pattern and a Slate transformation.
- trigger / before / after
- The plugin checks the rules when you type the trigger at a caret. A before pattern can match a prefix such as ##. For paired markers like **bold**, before matches the opening marker and after matches the closing marker. In that case, match.cleanText contains the text between them.
- change(editor, match)
- A match consumes the trigger. Your callback removes the marker text and applies the formatting. The example removes ##, then sets the block type supplied by HeadingsPlugin. Rules are tried in order, so put more specific patterns first.
- ignoreNodes
- List the block types where shortcuts should leave typed text alone. The demo skips code blocks so Markdown markers can be typed as code.
Setup example
Add the plugin instance to the plugins array you pass to KonaEditor.
import {
HeadingsPlugin,
ShortcutsPlugin,
type CustomElement,
} from '@use-kona/editor';
import { Editor, Transforms } from 'slate';
const shortcuts = new ShortcutsPlugin({
shortcuts: [
{
trigger: ' ',
before: /^##$/,
change(editor, { before }) {
if (!before || !editor.selection) return;
Editor.withoutNormalizing(editor, () => {
Transforms.delete(editor, {
distance: before[0].length,
reverse: true,
unit: 'character',
});
Transforms.setNodes<CustomElement>(editor, {
type: HeadingsPlugin.HeadingLevel2,
});
});
},
},
],
});MenuPlugin
Adds a toolbar for headings, lists, code blocks, and attachments.
How to use
Use the toolbar at the top of the editor to change the current block or attach a file. For example, put the cursor in a paragraph and choose Numbered list.
How it works
- renderMenu({ editor })
- Return the toolbar’s React elements. The editor argument is the active Slate editor. This plugin mounts the toolbar in editable mode. Each button supplies its own action.
- Preserving the selection
- Prevent the button’s default mouse-down behavior so the editor keeps its selection. Use onClick to run the action for mouse and keyboard activation. The example requires HeadingsPlugin.
Setup example
Add the plugin instance to the plugins array you pass to KonaEditor.
import { HeadingsPlugin, MenuPlugin } from '@use-kona/editor';
const toolbar = new MenuPlugin({
renderMenu: ({ editor }) => (
<button
type="button"
aria-pressed={HeadingsPlugin.isHeading2Active(editor)}
onMouseDown={(event) => event.preventDefault()}
onClick={() => HeadingsPlugin.toggleHeading2(editor)}
>
Heading 2
</button>
),
});DnDPlugin
Selects and reorders blocks with drag handles.
How to use
Hover over a block to reveal its handle. Click the handle to select the block, or drag it to move the block above or below another one.
How it works
- DndProvider
- Wrap KonaEditor in the react-dnd provider with HTML5Backend. The provider supplies the drag-and-drop context.
- renderBlock({ props, dragRef, dropRef, previewRef, … })
- Keep Slate’s attributes, ref, and children on the editable block. Connect dragRef to the handle and dropRef and previewRef to the block. Use position to draw a drop indicator and onToggleSelected to select the block when its handle is clicked.
- onDropFiles(editor, files, path)
- The app decides what to do with external files at the supplied Slate path. The example focuses on reordering blocks. To accept files, register AttachmentsPlugin and use the insertFiles function from its guide as onDropFiles.
Setup example
Add the plugin instance to the plugins array you pass to KonaEditor.
import {
DnDPlugin,
KonaEditor,
type CustomElement,
} from '@use-kona/editor';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { useState } from 'react';
const initialValue: CustomElement[] = [
{ type: 'paragraph', children: [{ text: 'First block' }] },
{ type: 'paragraph', children: [{ text: 'Second block' }] },
];
function Example() {
const [plugins] = useState(() => [
new DnDPlugin({
onDropFiles: () => {},
renderBlock: ({
props, dragRef, dropRef, previewRef, position,
onNativeDrop, onToggleSelected,
}) => (
<div
{...props.attributes}
ref={(node) => {
props.attributes.ref(node);
dropRef(node);
previewRef(node);
}}
onDrop={onNativeDrop}
style={{
borderTop: position === 'top' ? '2px solid blue' : undefined,
borderBottom: position === 'bottom' ? '2px solid blue' : undefined,
}}
>
<div
ref={(node) => { dragRef(node); }}
contentEditable={false}
onClick={onToggleSelected}
>
<button type="button" aria-label="Select or drag block">⠿</button>
</div>
{props.children}
</div>
),
}),
]);
return (
<DndProvider backend={HTML5Backend}>
<KonaEditor
initialValue={initialValue}
onChange={() => {}}
plugins={plugins}
/>
</DndProvider>
);
}AttachmentsPlugin
Attaches local files, previews images, and handles pasted images.
How to use
Use the paperclip in the toolbar to pick a file, or paste an image into the editor. Try a screenshot to see an inline preview. Demo files stay in your browser.
How it works
- Attachment({ element })
- Provide a React component that displays the file metadata saved in the Slate node. The plugin wraps it as a non-editable block and supplies the required Slate attributes and children.
- onPaste(editor, file)
- The plugin forwards pasted images to this callback. Your app reads or uploads the file and inserts an ATTACHMENT_ELEMENT with its metadata. The same helper can handle a file input or a drag-and-drop insertion path.
- insertFiles(editor, files, path?)
- The demo reads files as data URLs. A Slate path or range reference follows the insertion point while the file is being read. Release it when reading finishes and skip insertion if the reference no longer points to a valid location.
Setup example
Add the plugin instance to the plugins array you pass to KonaEditor.
import {
AttachmentsPlugin,
type CustomElement,
} from '@use-kona/editor';
import { Editor, Transforms, type Path } from 'slate';
import type { RenderElementProps } from 'slate-react';
type FileInfo = { name: string; type: string; url: string };
function Attachment({ element }: RenderElementProps) {
const { file } = element as CustomElement & { file: FileInfo };
return file.type.startsWith('image/') ? (
<img src={file.url} alt={file.name} />
) : (
<a href={file.url} download={file.name}>{file.name}</a>
);
}
function readFile(file: File): Promise<FileInfo> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve({
name: file.name,
type: file.type,
url: String(reader.result),
});
reader.onerror = () => reject(reader.error);
reader.onabort = () => reject(new Error('File reading cancelled'));
reader.readAsDataURL(file);
});
}
async function insertFiles(editor: Editor, files: File[], path?: Path) {
if (!files.length) return;
const ref = path
? Editor.pathRef(editor, path)
: editor.selection && Editor.rangeRef(editor, editor.selection);
try {
const attachments = await Promise.all(files.map(readFile));
if (ref && !ref.current) return;
Transforms.insertNodes(
editor,
attachments.map((file) => ({
type: AttachmentsPlugin.ATTACHMENT_ELEMENT,
file,
children: [{ text: '' }],
})),
{ at: ref?.current ?? [editor.children.length] },
);
} catch (error) {
console.error('Could not read the attachment', error);
} finally {
ref?.unref();
}
}
const attachments = new AttachmentsPlugin({
Attachment,
onPaste: (editor, file) => { void insertFiles(editor, [file]); },
});BreaksPlugin
Continues headings as paragraphs and adds line breaks with Shift+Enter.
How to use
Press Enter at the end of a heading to start a paragraph. Press Shift+Enter within a paragraph to put text on a new line in the same block.
How it works
- breakNodes
- List the block types that should continue as plain paragraphs when you press Enter. Register HeadingsPlugin if you use its block types. Other block types keep their usual Enter behavior.
- Shift+Enter
- The plugin inserts a newline into the current block. The block retains its type.
Setup example
Add the plugin instance to the plugins array you pass to KonaEditor.
import { BreaksPlugin, HeadingsPlugin } from '@use-kona/editor';
const breaks = new BreaksPlugin({
breakNodes: [
HeadingsPlugin.HeadingLevel1,
HeadingsPlugin.HeadingLevel2,
HeadingsPlugin.HeadingLevel3,
],
});CommandsPlugin
Opens searchable, nested commands when you type /.
How to use
Type / to open the command menu. Search for “code” to insert a code block, or open Headings to see its three heading commands.
How it works
- commands / getCommands()
- Supply an array of command entries. Each has a stable name, a visible title, a searchable commandName, and an icon. The demo builds the top-level array in its own getCommands() helper. The getCommands callback on an entry returns that entry’s submenu, synchronously or asynchronously. Register HeadingsPlugin for the heading action in this example.
- action(actions, editor)
- Run the operation when a command is chosen. actions.set, actions.insert, and actions.wrap remove the typed slash and query before changing the document. For a custom editor action, call actions.removeCommand() yourself first.
- renderMenu(children)
- Use (children) => children to keep the default look, or wrap the children to apply styles. The plugin supplies the positioned menu, command buttons, backdrop, and keyboard handling. It renders the result under document.body, so put theme variables on the wrapper if they are scoped to the editor.
- render / ignoreNodes
- Set render on an individual command to customize the contents of its button. Set ignoreNodes on the plugin to suppress the popup in specific block types, such as code lines.
Setup example
Add the plugin instance to the plugins array you pass to KonaEditor.
import { CommandsPlugin, HeadingsPlugin } from '@use-kona/editor';
const commands = new CommandsPlugin({
commands: [
{
name: 'headings',
title: 'Headings',
commandName: 'headings',
icon: <span aria-hidden="true">H</span>,
getCommands: () => [
{
name: 'heading-2',
title: 'Heading 2',
commandName: 'heading2',
icon: <span aria-hidden="true">H₂</span>,
action: (actions) => {
actions.set({ type: HeadingsPlugin.HeadingLevel2 });
},
},
],
},
],
renderMenu: (children) => (
<div style={{ fontFamily: 'system-ui' }}>{children}</div>
),
});HighlightsPlugin
Highlights selected text in red, blue, or yellow.
How to use
Select a phrase, open Highlight color in the floating menu, and choose red, blue, or yellow. Use Remove highlight to clear the color.
How it works
- colors
- Map the color names you want to store in the document to CSS color values. This example stores "yellow" in the text’s highlight mark and renders it using the configured hex color.
- toggleHighlight / removeHighlight
- Wire these methods to your own menu buttons. Pass the configured color key when toggling, or removeHighlight(editor) to clear the mark.
Setup example
Add the plugin instance to the plugins array you pass to KonaEditor.
import { HighlightsPlugin } from '@use-kona/editor';
import type { Editor } from 'slate';
const highlights = new HighlightsPlugin({
colors: {
red: '#f3d6d6',
blue: '#d6e6f3',
yellow: '#f3e8b5',
},
});
function toggleYellowHighlight(editor: Editor) {
HighlightsPlugin.toggleHighlight(editor, 'yellow');
}TableOfContentsPlugin
Builds a heading outline for navigating the document.
How to use
Hover over the short lines at the top right of the editor to reveal the outline. Select an entry such as “Try the plugins” to jump to that heading.
How it works
- levels
- Map registered block types to outline depths from 1 to 3. Register HeadingsPlugin for the h1, h2, and h3 types used here. The plugin builds the outline from matching blocks.
- renderEmoji(id)
- If headings can contain emoji nodes, supply the same ID renderer you use in EmojiPlugin so the outline shows the same emoji.
Setup example
Add the plugin instance to the plugins array you pass to KonaEditor.
import {
HeadingsPlugin,
TableOfContentsPlugin,
} from '@use-kona/editor';
const outline = new TableOfContentsPlugin({
levels: {
[HeadingsPlugin.HeadingLevel1]: 1,
[HeadingsPlugin.HeadingLevel2]: 2,
[HeadingsPlugin.HeadingLevel3]: 3,
},
});FloatingMenuPlugin
Shows formatting, link, and highlight controls when you select text.
How to use
Select a few words in a paragraph to open the menu beside the selection. For example, select “Easy to extend” and use the menu to underline it.
How it works
- renderMenu(editor, controls)
- Build the buttons or inputs inside the selection popup. The plugin handles where the popup appears. The example uses BasicFormattingPlugin to make the selected text bold.
- onShow() / onHide()
- The return value is the desired visibility. Return true when there is an eligible selection and false to hide when it goes away. A menu with an input can return true from onHide while that input has focus.
- controls.onClose() / controls.onUpdate()
- Use onClose to hide the current popup after an action. Use onUpdate to reposition it after the menu contents change size, for example when switching to a link input.
Setup example
Add the plugin instance to the plugins array you pass to KonaEditor.
import {
BasicFormattingPlugin,
FloatingMenuPlugin,
} from '@use-kona/editor';
const selectionMenu = new FloatingMenuPlugin({
renderMenu: (editor, { onClose }) => (
<button
type="button"
onClick={() => {
BasicFormattingPlugin.toggleBold(editor);
onClose();
}}
>
Bold
</button>
),
onShow: () => true,
onHide: () => false,
});