Editor.Toolbar Component

The Editor.Toolbar component provides a rich set of formatting controls, view mode toggles, and editing actions. It's context-aware, displaying active states based on the current block type and cursor position.

Import

tsximport { Editor } from "@/components/ui/markdown-editor"; // Use as <Editor.Toolbar />;

Overview

The toolbar is divided into four main sections:

  1. Formatting Actions (left) - Text and block formatting buttons
  2. Layout Controls (right) - Orientation toggle for split view
  3. View Mode Toggles (right) - Edit, Split, Preview modes
  4. History Controls (right) - Undo and Redo buttons

Props

actions

  • Type: ToolbarAction[]
  • Required: Yes
  • Description: Array of formatting actions to display in the toolbar
tsxinterface ToolbarAction { id: string; label: string; icon: React.ReactNode; shortcut?: string; handler: ( content: string, selection: SelectionState, ) => { content: string; selection: SelectionState; }; }

Example:

tsximport { createToolbarActions } from "@/components/ui/markdown-editor"; const actions = createToolbarActions(); <Editor.Toolbar actions={actions} onAction={handleAction} currentBlock={currentBlock} undo={undo} redo={redo} canUndo={canUndo} canRedo={canRedo} />;

onAction

  • Type: (action: ToolbarAction) => void
  • Required: Yes
  • Description: Callback fired when a formatting button is clicked
tsxconst handleAction = (action: ToolbarAction) => { const result = action.handler(content, selection); updateContent(result.content, result.selection); // Restore focus to editor textareaRef.current?.focus(); }; <Editor.Toolbar actions={actions} onAction={handleAction} // ... other props />;
tsxconst { content, selection, updateContent } = useEditor();

currentBlock

  • Type: Block | null
  • Required: Yes
  • Description: The block at the current cursor position, used for context-aware button states
tsxinterface Block { id: string; type: BlockType; // 'paragraph' | 'heading' | 'code' | 'quote' | 'list' level?: number; // For headings (1-6) startOffset: number; endOffset: number; content: string; }

Usage:

tsxconst currentBlock = getCurrentBlock(blocks, selection.start); <Editor.Toolbar actions={actions} onAction={handleAction} currentBlock={currentBlock} // Highlights active block type // ... other props />;

Effect:

  • When cursor is in a heading, the corresponding H1/H2/H3 button is highlighted
  • When cursor is in a code block, the code button appears active
  • Provides visual feedback about current context

undo

  • Type: () => void
  • Required: Yes
  • Description: Callback to undo the last change
tsx// Example implementation const undo = () => { if (historyIndex > 0) { setHistoryIndex((prev) => prev - 1); setContent(history[historyIndex - 1]); } }; <Editor.Toolbar actions={actions} onAction={handleAction} currentBlock={currentBlock} undo={undo} // ... other props />;
tsxconst { undo } = useEditor();

redo

  • Type: () => void
  • Required: Yes
  • Description: Callback to redo the last undone change
tsx// Example implementation const redo = () => { if (historyIndex < history.length - 1) { setHistoryIndex((prev) => prev + 1); setContent(history[historyIndex + 1]); } }; <Editor.Toolbar actions={actions} onAction={handleAction} currentBlock={currentBlock} undo={undo} redo={redo} // ... other props />;
tsxconst { undo, redo } = useEditor();

canUndo

  • Type: boolean
  • Required: Yes
  • Description: Whether undo action is available (disables button when false)
tsx// Example implementation const canUndo = historyIndex > 0; <Editor.Toolbar actions={actions} onAction={handleAction} currentBlock={currentBlock} undo={undo} redo={redo} canUndo={canUndo} // Button disabled when false // ... other props />;
tsxconst { canUndo } = useEditor();

canRedo

  • Type: boolean
  • Required: Yes
  • Description: Whether redo action is available (disables button when false)
tsx// Example implementation const canRedo = historyIndex < history.length - 1; <Editor.Toolbar actions={actions} onAction={handleAction} currentBlock={currentBlock} undo={undo} redo={redo} canUndo={canUndo} canRedo={canRedo} // Button disabled when false />;
tsxconst { canRedo } = useEditor();

mode

  • Type: 'edit' | 'preview' | 'split'
  • Optional: Yes
  • Description: Current view mode of the editor
tsxconst [mode, setMode] = useState<ViewMode>("split"); <Editor.Toolbar actions={actions} onAction={handleAction} currentBlock={currentBlock} undo={undo} redo={redo} canUndo={canUndo} canRedo={canRedo} mode={mode} setMode={setMode} />;

Effect:

  • Highlights the active view mode button
  • Disables formatting buttons in preview mode (no editing allowed)

setMode

  • Type: (mode: ViewMode) => void
  • Optional: Yes
  • Description: Callback to change the view mode
tsxconst handleModeChange = (newMode: ViewMode) => { setMode(newMode); // Optional: Save preference localStorage.setItem("editorMode", newMode); }; <Editor.Toolbar actions={actions} onAction={handleAction} currentBlock={currentBlock} undo={undo} redo={redo} canUndo={canUndo} canRedo={canRedo} mode={mode} setMode={handleModeChange} />;

orientation

  • Type: 'horizontal' | 'vertical'
  • Optional: Yes
  • Default: 'horizontal'
  • Description: Layout orientation for split view
tsxconst [orientation, setOrientation] = useState<"horizontal" | "vertical">( "horizontal", ); <Editor.Toolbar actions={actions} onAction={handleAction} currentBlock={currentBlock} undo={undo} redo={redo} canUndo={canUndo} canRedo={canRedo} orientation={orientation} setOrientation={setOrientation} />;

Effect:

  • 'horizontal' → Editor and preview side-by-side
  • 'vertical' → Editor and preview stacked vertically
  • Shows <GalleryHorizontal /> icon when horizontal
  • Shows <GalleryVertical /> icon when vertical

setOrientation

  • Type: (orientation: 'horizontal' | 'vertical') => void
  • Optional: Yes
  • Description: Callback to change the split view orientation
tsxconst handleOrientationChange = (newOrientation: "horizontal" | "vertical") => { setOrientation(newOrientation); }; <Editor.Toolbar actions={actions} onAction={handleAction} currentBlock={currentBlock} undo={undo} redo={redo} canUndo={canUndo} canRedo={canRedo} orientation={orientation} setOrientation={handleOrientationChange} />;

fullScreen

  • Type: boolean
  • Optional: Yes
  • Default: false
  • Description: Enable fullScreen view or not
tsxconst [fullScreen, setFullScreen] = useState(false); <Editor.Toolbar actions={actions} onAction={handleAction} currentBlock={currentBlock} undo={undo} redo={redo} canUndo={canUndo} canRedo={canRedo} fullScreen={fullScreen} />;

Effect:

  • True → Editor is in fullScreen view mode
  • False → Editor is in the normal view mode

setFullScreen

  • Type: () => void
  • Optional: Yes
  • Description: Callback to change the fullScreen mode
tsxconst [fullScreen, setFullScreen] = useState(false); <Editor.Toolbar actions={actions} onAction={handleAction} currentBlock={currentBlock} undo={undo} redo={redo} canUndo={canUndo} canRedo={canRedo} fullScreen={fullScreen} setFullScreen={() => setFullScreen(!fullScreen)} />;

Basic Usage

Minimal Setup

tsx"use client"; import { Editor, createToolbarActions, useEditor, } from "@/components/ui/markdown-editor"; import { useRef, useState } from "react"; export default function MinimalToolbar() { const textareaRef = useRef<HTMLTextAreaElement>(null); const actions = createToolbarActions(); const { content, selection, currentBlock, setSelection, updateContent, undo, redo, canUndo, canRedo, } = useEditor("# Hello"); const handleAction = (action: any) => { const result = action.handler(content, selection); updateContent(result.content, result.selection); }; return ( <div className="h-[400px] flex flex-col"> <Editor.Toolbar actions={actions} onAction={handleAction} currentBlock={currentBlock} undo={undo} redo={redo} canUndo={canUndo} canRedo={canRedo} /> <Editor.Core content={content} onChange={updateContent} onSelectionChange={setSelection} textareaRef={textareaRef} /> </div> ); }

Preview

With View Modes

tsx"use client"; import { Editor, createToolbarActions, useEditor, ViewMode, } from "@/components/ui/markdown-editor"; import { useRef, useState } from "react"; export default function ToolbarWithModes() { const textareaRef = useRef<HTMLTextAreaElement>(null); const actions = createToolbarActions(); const [mode, setMode] = useState<ViewMode>("split"); const { content, selection, currentBlock, updateContent, setSelection, undo, redo, canUndo, canRedo, } = useEditor("# Content"); const handleAction = (action: any) => { const result = action.handler(content, selection); updateContent(result.content, result.selection); }; return ( <div className="h-screen flex flex-col"> <Editor.Toolbar actions={actions} onAction={handleAction} currentBlock={currentBlock} undo={undo} redo={redo} canUndo={canUndo} canRedo={canRedo} mode={mode} setMode={setMode} /> <div className="flex-1 flex"> {(mode === "edit" || mode === "split") && ( <Editor.Core content={content} onChange={updateContent} onSelectionChange={setSelection} textareaRef={textareaRef} /> )} {(mode === "preview" || mode === "split") && ( <Editor.Preview content={content} /> )} </div> </div> ); }

Preview

Content

With Orientation Toggle

tsx"use client"; import { Editor, createToolbarActions, useEditor, ViewMode, Orientation, } from "@/components/ui/markdown-editor"; import { useRef, useState } from "react"; export default function ToolbarWithOrientation() { const textareaRef = useRef<HTMLTextAreaElement>(null); const actions = createToolbarActions(); const [mode, setMode] = useState<ViewMode>("split"); const [orientation, setOrientation] = useState<Orientation>("horizontal"); const { content, selection, currentBlock, updateContent, setSelection, undo, redo, canUndo, canRedo, } = useEditor("# Content"); const handleAction = (action: any) => { const result = action.handler(content, selection); updateContent(result.content, result.selection); }; return ( <div className="h-[500px] flex flex-col"> <Editor.Toolbar actions={actions} onAction={handleAction} currentBlock={currentBlock} undo={undo} redo={redo} canUndo={canUndo} canRedo={canRedo} mode={mode} setMode={setMode} orientation={orientation} setOrientation={setOrientation} /> <div className={`flex-1 flex ${orientation === "horizontal" ? "flex-row" : "flex-col"}`} > {(mode === "edit" || mode === "split") && ( <Editor.Core content={content} onChange={updateContent} onSelectionChange={setSelection} textareaRef={textareaRef} /> )} {(mode === "preview" || mode === "split") && ( <Editor.Preview content={content} /> )} </div> </div> ); }

Preview

Content

Toolbar Sections

1. Formatting Actions (Left)

First Group (Inline Formatting):

  • Bold - Wraps selection in **text**
  • Italic - Wraps selection in _text_
  • Inline Code - Wraps selection in `code`
  • Link - Creates [text](url) format

Second Group (Block Formatting):

  • H1 - Converts line to # Heading
  • H2 - Converts line to ## Heading
  • H3 - Converts line to ### Heading
  • Quote - Converts line to > Quote
  • Code Block - Wraps selection in ```code```

Behavior:

  • Buttons are disabled when mode === 'preview'
  • Active button is highlighted based on currentBlock type
  • Each button shows tooltip with label and keyboard shortcut

2. Layout Controls (Right)

Orientation Toggle:

  • Switches between horizontal and vertical split layouts
  • Only affects layout when mode === 'split'

3. View Mode Toggles (Right)

Three Modes:

  1. Edit

    • Shows only the editor
    • Full width for writing
    • Preview hidden
  2. Split

    • Shows editor and preview side-by-side (or stacked)
    • Default mode
    • Best for writing with live feedback
  3. Preview

    • Shows only the preview
    • Full width for reading
    • Formatting buttons disabled

4. History Controls (Right)

Undo Button:

  • Tooltip: "Undo (⌘Z)"
  • Disabled when canUndo is false
  • Reverts last change

Redo Button:

  • Tooltip: "Redo (⌘⇧Z)"
  • Disabled when canRedo is false
  • Reapplies last undone change

Toolbar Actions

Default Actions

Created by createToolbarActions():

tsxconst actions = [ { id: "bold", label: "Bold", icon: <Bold />, shortcut: "⌘B", handler: (content, selection) => wrapSelection(content, selection, "**"), }, { id: "italic", label: "Italic", icon: <Italic />, shortcut: "⌘I", handler: (content, selection) => wrapSelection(content, selection, "_"), }, { id: "code", label: "Inline Code", icon: <CodeXml />, handler: (content, selection) => wrapSelection(content, selection, "`"), }, { id: "link", label: "Link", icon: <Link2 />, shortcut: "⌘K", handler: (content, selection) => { const selectedText = content.slice(selection.start, selection.end) || "link text"; const linkMarkdown = `[${selectedText}](url)`; // ... implementation }, }, // Block formatting actions... ];

Custom Actions

Add your own formatting actions:

tsximport { createToolbarActions, type ToolbarAction, } from "@/components/ui/markdown-editor"; import { Strikethrough } from "lucide-react"; const defaultActions = createToolbarActions(); const strikethroughAction: ToolbarAction = { id: "strikethrough", label: "Strikethrough", icon: <Strikethrough />, handler: (content, selection) => { const before = content.slice(0, selection.start); const selected = content.slice(selection.start, selection.end); const after = content.slice(selection.end); return { content: before + "~~" + selected + "~~" + after, selection: { start: selection.start + 2, end: selection.end + 2, }, }; }, }; const customActions = [...defaultActions, strikethroughAction]; <Editor.Toolbar actions={customActions} onAction={handleAction} // ... other props />;

Context-Aware Highlighting

The toolbar automatically highlights buttons based on cursor position:

tsx// When cursor is in: // Heading 1 (# text) currentBlock = { type: 'heading', level: 1, ... } // → H1 button is highlighted // Heading 2 (## text) currentBlock = { type: 'heading', level: 2, ... } // → H2 button is highlighted // Code block (```code```) currentBlock = { type: 'code', ... } // → Code Block button is highlighted // Quote (> text) currentBlock = { type: 'quote', ... } // → Quote button is highlighted // Regular paragraph currentBlock = { type: 'paragraph', ... } // → No block format buttons highlighted

Disabled States

Preview Mode

When mode === 'preview', all formatting actions are disabled:

tsx<Button onClick={() => onAction(action)} disabled={mode === "preview"} // ← Prevents formatting in preview // ... other props > {action.icon} </Button>

Why?

  • No editor visible in preview mode
  • Prevents confusion when users can't see changes
  • Formatting only works in edit/split modes

History Buttons

Undo/Redo buttons are disabled based on history state:

tsx<Button onClick={undo} disabled={!canUndo} // ← Disabled at start of history // ... props /> <Button onClick={redo} disabled={!canRedo} // ← Disabled at end of history // ... props />

Keyboard Shortcuts

The toolbar displays keyboard shortcut hints in tooltips:

ActionShortcutTooltip
Bold⌘B"Bold (⌘B)"
Italic⌘I"Italic (⌘I)"
Link⌘K"Link (⌘K)"
Undo⌘Z"Undo (⌘Z)"
Redo⌘⇧Z"Redo (⌘⇧Z)"

Examples

Custom Toolbar with Extra Actions

tsximport { createToolbarActions, type ToolbarAction, } from "@/components/ui/markdown-editor"; import { Table, Image } from "lucide-react"; const tableAction: ToolbarAction = { id: "table", label: "Insert Table", icon: <Table />, handler: (content, selection) => { const table = "\n| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1 | Cell 2 |\n"; return { content: content.slice(0, selection.start) + table + content.slice(selection.end), selection: { start: selection.start + 1, end: selection.start + 1 }, }; }, }; const imageAction: ToolbarAction = { id: "image", label: "Insert Image", icon: <Image />, handler: (content, selection) => { const markdown = "![alt text](image-url)"; return { content: content.slice(0, selection.start) + markdown + content.slice(selection.end), selection: { start: selection.start + 2, end: selection.start + 10 }, }; }, }; const actions = [...createToolbarActions(), tableAction, imageAction]; <Editor.Toolbar actions={actions} onAction={handleAction} currentBlock={currentBlock} undo={undo} redo={redo} canUndo={canUndo} canRedo={canRedo} />;

Toolbar with Saved Preferences

tsx"use client"; import { Editor, createToolbarActions, useEditor, } from "@/components/ui/markdown-editor"; import { useState, useEffect } from "react"; export default function PersistentToolbar() { const [mode, setMode] = useState<"edit" | "preview" | "split">("split"); const [orientation, setOrientation] = useState<"horizontal" | "vertical">( "horizontal", ); // Load preferences useEffect(() => { const savedMode = localStorage.getItem("editorMode"); const savedOrientation = localStorage.getItem("editorOrientation"); if (savedMode) setMode(savedMode as any); if (savedOrientation) setOrientation(savedOrientation as any); }, []); // Save preferences const handleModeChange = (newMode: any) => { setMode(newMode); localStorage.setItem("editorMode", newMode); }; const handleOrientationChange = (newOrientation: any) => { setOrientation(newOrientation); localStorage.setItem("editorOrientation", newOrientation); }; // ... rest of implementation return ( <Editor.Toolbar actions={createToolbarActions()} onAction={handleAction} currentBlock={currentBlock} undo={undo} redo={redo} canUndo={canUndo} canRedo={canRedo} mode={mode} setMode={handleModeChange} orientation={orientation} setOrientation={handleOrientationChange} /> ); }

Accessibility

The toolbar includes:

  • ✅ Tooltips on all buttons (via title attribute)
  • ✅ Keyboard shortcuts displayed
  • ✅ Visual disabled states
  • ✅ Icon-only buttons with descriptive titles
  • ✅ Logical tab order

Future improvements:

  • Add ARIA labels for better screen reader support
  • Implement keyboard navigation between toolbar buttons

Integration with MarkdownEditor

The toolbar is automatically included in MarkdownEditor:

tsx<MarkdownEditor /> // Includes Editor.Toolbar with all features

To use standalone, you need to provide all required props from useEditor hook.