The fastest way to add toolbar functionality is with the FixedToolbarKit and FloatingToolbarKit, which include pre-configured toolbar plugins along with their Plate UI components.
'use client';
import {
BoldPlugin,
CodePlugin,
HighlightPlugin,
ItalicPlugin,
StrikethroughPlugin,
UnderlinePlugin,
} from '@platejs/basic-nodes/react';
import {
FontBackgroundColorPlugin,
FontColorPlugin,
} from '@platejs/basic-styles/react';
import {
BaseAudioPlugin,
BaseFilePlugin,
BaseImagePlugin,
BaseVideoPlugin,
} from '@platejs/media';
import {
BaselineIcon,
BoldIcon,
Code2Icon,
HighlighterIcon,
ItalicIcon,
PaintBucketIcon,
StrikethroughIcon,
UnderlineIcon,
WandSparklesIcon,
} from 'lucide-react';
import { useEditorReadOnly, definePlatePlugin } from 'platejs/react';
import * as React from 'react';
import { cn } from '@/lib/utils';
import { ToolbarGroup, Toolbar } from '@/components/editor/toolbar';
import { AIToolbarButton } from './ai-toolbar-button';
import { AlignToolbarButton } from './align-toolbar-button';
import { CommentToolbarButton } from './comment-toolbar-button';
import { EmojiToolbarButton } from './emoji-toolbar-button';
import {
DEFAULT_COLORS,
FontColorToolbarButton,
} from './font-color-toolbar-button';
import { FontSizeToolbarButton } from './font-size-toolbar-button';
import { RedoToolbarButton, UndoToolbarButton } from './history-toolbar-button';
import {
IndentToolbarButton,
OutdentToolbarButton,
} from './indent-toolbar-button';
import { InsertToolbarButton } from './insert-toolbar-button';
import { LineHeightToolbarButton } from './line-height-toolbar-button';
import { LinkToolbarButton } from './link-toolbar-button';
import {
BulletedListToolbarButton,
NumberedListToolbarButton,
TodoListToolbarButton,
} from './list-toolbar-button';
import { MarkToolbarButton } from './mark-toolbar-button';
import { MediaToolbarButton } from './media-toolbar-button';
import { ModeToolbarButton } from './mode-toolbar-button';
import { MoreToolbarButton } from './more-toolbar-button';
import { TableToolbarButton } from './table-toolbar-button';
import { ToggleToolbarButton } from './toggle-toolbar-button';
import { TurnIntoToolbarButton } from './turn-into-toolbar-button';
export function FixedToolbarButtons({
children,
}: {
children?: React.ReactNode;
} = {}) {
const readOnly = useEditorReadOnly();
return (
<div className="flex w-full">
{!readOnly && (
<>
<ToolbarGroup>
<UndoToolbarButton />
<RedoToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<AIToolbarButton tooltip="AI commands">
<WandSparklesIcon />
</AIToolbarButton>
</ToolbarGroup>
{children}
<ToolbarGroup>
<InsertToolbarButton />
<TurnIntoToolbarButton />
<FontSizeToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<MarkToolbarButton plugin={BoldPlugin} tooltip="Bold (⌘+B)">
<BoldIcon />
</MarkToolbarButton>
<MarkToolbarButton plugin={ItalicPlugin} tooltip="Italic (⌘+I)">
<ItalicIcon />
</MarkToolbarButton>
<MarkToolbarButton
plugin={UnderlinePlugin}
tooltip="Underline (⌘+U)"
>
<UnderlineIcon />
</MarkToolbarButton>
<MarkToolbarButton
plugin={StrikethroughPlugin}
tooltip="Strikethrough (⌘+⇧+M)"
>
<StrikethroughIcon />
</MarkToolbarButton>
<MarkToolbarButton plugin={CodePlugin} tooltip="Code (⌘+E)">
<Code2Icon />
</MarkToolbarButton>
<FontColorToolbarButton
colors={DEFAULT_COLORS}
plugin={FontColorPlugin}
tooltip="Text color"
>
<BaselineIcon />
</FontColorToolbarButton>
<FontColorToolbarButton
colors={DEFAULT_COLORS}
plugin={FontBackgroundColorPlugin}
tooltip="Background color"
>
<PaintBucketIcon />
</FontColorToolbarButton>
</ToolbarGroup>
<ToolbarGroup>
<AlignToolbarButton />
<NumberedListToolbarButton />
<BulletedListToolbarButton />
<TodoListToolbarButton />
<ToggleToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<LinkToolbarButton />
<TableToolbarButton />
<EmojiToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<MediaToolbarButton plugin={BaseImagePlugin} />
<MediaToolbarButton plugin={BaseVideoPlugin} />
<MediaToolbarButton plugin={BaseAudioPlugin} />
<MediaToolbarButton plugin={BaseFilePlugin} />
</ToolbarGroup>
<ToolbarGroup>
<LineHeightToolbarButton />
<OutdentToolbarButton />
<IndentToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<MoreToolbarButton />
</ToolbarGroup>
</>
)}
<div className="grow" />
<ToolbarGroup>
<MarkToolbarButton plugin={HighlightPlugin} tooltip="Highlight">
<HighlighterIcon />
</MarkToolbarButton>
<CommentToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<ModeToolbarButton />
</ToolbarGroup>
</div>
);
}
export function FixedToolbar(props: React.ComponentProps<typeof Toolbar>) {
return (
<Toolbar
{...props}
className={cn(
'scrollbar-hide sticky top-0 left-0 z-50 w-full justify-between overflow-x-auto rounded-t-lg border-b border-b-border bg-background/95 p-1 backdrop-blur-sm supports-backdrop-blur:bg-background/60',
props.className
)}
/>
);
}
export const FixedToolbarPlugin = definePlatePlugin('fixedToolbar', {
render: {
beforeEditable: () => (
<FixedToolbar>
<FixedToolbarButtons />
</FixedToolbar>
),
},
});
export const FixedToolbarKit = [FixedToolbarPlugin] as const;'use client';
import {
BoldPlugin,
CodePlugin,
HighlightPlugin,
ItalicPlugin,
StrikethroughPlugin,
UnderlinePlugin,
} from '@platejs/basic-nodes/react';
import {
FontBackgroundColorPlugin,
FontColorPlugin,
} from '@platejs/basic-styles/react';
import {
BaseAudioPlugin,
BaseFilePlugin,
BaseImagePlugin,
BaseVideoPlugin,
} from '@platejs/media';
import {
BaselineIcon,
BoldIcon,
Code2Icon,
HighlighterIcon,
ItalicIcon,
'use client';
import { AIChatPlugin } from '@platejs/ai/react';
import {
BoldPlugin,
CodePlugin,
ItalicPlugin,
StrikethroughPlugin,
UnderlinePlugin,
} from '@platejs/basic-nodes/react';
import {
type UseVirtualFloatingOptions,
flip,
getSelectionBoundingClientRect,
offset,
useVirtualFloating,
} from '@platejs/floating';
import { useComposedRef } from '@udecode/cn';
import { useOnClickOutside } from '@udecode/react-utils';
import { mergeProps } from '@udecode/utils'
FixedToolbar: Renders a persistent toolbar above the editorFixedToolbarButtons: Pre-configured button set for the fixed toolbarFloatingToolbar: Renders a contextual toolbar on text selectionFloatingToolbarButtons: Pre-configured button set for the floating toolbarimport { createPlateEditor } from 'platejs/react';
import { FixedToolbarKit } from '@/components/editor/fixed-toolbar';
import { FloatingToolbarKit } from '@/components/editor/floating-toolbar';
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
...FixedToolbarKit,
...FloatingToolbarKit,
],
});import { createPlateEditor } from 'platejs/react'
import { definePlatePlugin } from 'platejs/react';
import { FixedToolbar } from '@/components/editor/fixed-toolbar';
import { FixedToolbarButtons } from '@/components/editor/fixed-toolbar';
import { FloatingToolbar } from '@/components/editor/floating-toolbar';
import { FloatingToolbarButtons } from '@/components/editor/floating-toolbar';
const fixedToolbarPlugin = definePlatePlugin('fixedToolbar', {
render: {
beforeEditable: () => (
<FixedToolbar>
<FixedToolbarButtons />
</FixedToolbar>
),
},
});
const floatingToolbarPlugin = definePlatePlugin('floatingToolbar', {
render: {
afterEditable: () => (
<FloatingToolbar>
<FloatingToolbarButtons />
</FloatingToolbar>
),
},
});
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
fixedToolbarPlugin,
floatingToolbarPlugin,
],
});import { definePlatePlugin } from 'platejs/react';
import { FixedToolbar } from '@/components/editor/fixed-toolbar';
import { FixedToolbarButtons } from '@/components/editor/fixed-toolbar';
import { FloatingToolbar } from '@/components/editor/floating-toolbar';
import { FloatingToolbarButtons } from '@/components/editor/floating-toolbar';
const fixedToolbarPlugin = definePlatePlugin('fixedToolbar', {
render: {
beforeEditable: () => (
<FixedToolbar>
<FixedToolbarButtons />
</FixedToolbar>
),
},
});
render.beforeEditable: Renders FixedToolbar above the editor contentrender.afterEditable: Renders FloatingToolbar as an overlay after the editorThe FixedToolbarButtons component contains the default set of buttons for the fixed toolbar.
'use client';
import {
BoldPlugin,
CodePlugin,
HighlightPlugin,
ItalicPlugin,
StrikethroughPlugin,
UnderlinePlugin,
} from '@platejs/basic-nodes/react';
import {
FontBackgroundColorPlugin,
FontColorPlugin,
} from '@platejs/basic-styles/react';
import {
BaseAudioPlugin,
BaseFilePlugin,
BaseImagePlugin,
BaseVideoPlugin,
} from '@platejs/media';
import {
BaselineIcon,
BoldIcon,
Code2Icon,
HighlighterIcon,
To customize it, you can edit components/editor/fixed-toolbar.tsx.
Similarly, you can customize the floating toolbar by editing components/editor/floating-toolbar.tsx.
'use client';
import { AIChatPlugin } from '@platejs/ai/react';
import {
BoldPlugin,
CodePlugin,
ItalicPlugin,
StrikethroughPlugin,
UnderlinePlugin,
} from '@platejs/basic-nodes/react';
import {
type UseVirtualFloatingOptions,
flip,
getSelectionBoundingClientRect,
offset,
useVirtualFloating,
} from '@platejs/floating';
import { useComposedRef } from '@udecode/cn';
import { useOnClickOutside } from '@udecode/react-utils';
import { mergeProps } from '@udecode/utils'
This example shows a button that inserts custom text into the editor.
import { useEditor } from 'platejs/react';
import { CustomIcon } from 'lucide-react';
import { ToolbarButton } from '@/components/editor/toolbar';
export function CustomToolbarButton() {
const editor = useEditor();
return (
<ToolbarButton
onClick={() => {
// Custom action
editor.update((tx) => {
tx.text.insert('Custom text');
});
}}
tooltip="Custom Action"
For toggling marks like bold or italic, you can use the MarkToolbarButton component. It simplifies the process by handling the toggle state and action automatically.
This example creates a "Bold" button.
import { BoldIcon } from 'lucide-react';
import { MarkToolbarButton } from '@/components/editor/mark-toolbar-button';
export function BoldToolbarButton() {
return (
<MarkToolbarButton nodeType="bold" tooltip="Bold (⌘+B)">
<BoldIcon />
</MarkToolbarButton>
);
}import { BoldIcon } from
nodeType: Specifies the mark to toggle (e.g., bold, italic).tooltip: Provides a helpful tooltip for the button.MarkToolbarButton reads mark state and handles toggling directly in the component.The TurnIntoToolbarButton provides a dropdown menu to convert the current block into different types (headings, lists, quotes, etc.).
'use client';
import { ElementApi } from '@platejs/plite';
import { PLUGINS } from '@platejs/utils';
import {
ChevronRightIcon,
Code2Icon,
Columns3Icon,
FileCodeIcon,
Heading1Icon,
Heading2Icon,
Heading3Icon,
Heading4Icon,
Heading5Icon,
Heading6Icon,
ListIcon,
ListOrderedIcon,
PilcrowIcon,
QuoteIcon,
SquareIcon,
} from 'lucide-react';
import { useEditor, useSelectionFragmentProp } from 'platejs/react';
import * as React from
To add a new block type to the turn-into options, edit the turnIntoItems array:
const turnIntoItems = [
// ... existing items
{
icon: <CustomIcon />,
keywords: ['custom', 'special'],
label: 'Custom Block',
value: 'custom-block',
},
];const turnIntoItems = [
// ... existing items
{
icon: <CustomIcon />,
keywords: ['custom', 'special'],
The InsertToolbarButton provides a dropdown menu to insert various elements (blocks, lists, media, inline elements).
'use client';
import {
CalendarIcon,
ChevronRightIcon,
Code2,
Columns3Icon,
FileCodeIcon,
FilmIcon,
Heading1Icon,
Heading2Icon,
Heading3Icon,
ImageIcon,
Link2Icon,
ListIcon,
ListOrderedIcon,
MinusIcon,
PenToolIcon,
PilcrowIcon,
PlusIcon,
QuoteIcon,
RadicalIcon,
SquareIcon,
SuperscriptIcon,
TableIcon,
TableOfContentsIcon,
} from 'lucide-react';
import { PLUGINS } from 'platejs';
To add a new insertable item, add it to the appropriate group in the groups array:
{
group: 'Basic blocks',
items: [
// ... existing items
{
icon: <CustomIcon />,
label: 'Custom Block',
value: 'custom-block',
},
].map((item) => ({
...item,
onSelect: (editor, value) => {
insertBlock(editor, value);
},
})),
}Plugin that renders a fixed toolbar above the editor content.
Plugin that renders a floating toolbar that appears on text selection.
'use client';
import { AIChatPlugin } from '@platejs/ai/react';
import {
BoldPlugin,
CodePlugin,
ItalicPlugin,
StrikethroughPlugin,
UnderlinePlugin,
} from '@platejs/basic-nodes/react';
import {
type UseVirtualFloatingOptions,
flip,
getSelectionBoundingClientRect,
offset,
useVirtualFloating,
} from '@platejs/floating';
import { useComposedRef } from '@udecode/cn';
import { useOnClickOutside } from '@udecode/react-utils';
import { mergeProps } from '@udecode/utils';
import {
BoldIcon,
Code2Icon,
ItalicIcon,
StrikethroughIcon,
UnderlineIcon,
WandSparklesIcon,
} from 'lucide-react';
import {
useEditorReadOnly,
definePlatePlugin,
useEditor,
useEditorId,
useEditorSelector,
useEventEditorValue,
usePluginStore,
} from 'platejs/react';
import * as React from 'react';
import { cn } from '@/lib/utils';
import { ToolbarGroup, Toolbar } from '@/components/editor/toolbar';
import { AIToolbarButton } from './ai-toolbar-button';
import { CommentToolbarButton } from './comment-toolbar-button';
import { InlineEquationToolbarButton } from './equation-toolbar-button';
import { linkPlugin } from './link';
import { LinkToolbarButton } from './link-toolbar-button';
import { MarkToolbarButton } from './mark-toolbar-button';
import { MoreToolbarButton } from './more-toolbar-button';
import { SuggestionToolbarButton } from './suggestion-toolbar-button';
import { TurnIntoToolbarButton } from './turn-into-toolbar-button';
export function FloatingToolbarButtons() {
const readOnly = useEditorReadOnly();
return (
<>
{!readOnly && (
<>
<ToolbarGroup>
<AIToolbarButton tooltip="AI commands">
<WandSparklesIcon />
Ask AI
</AIToolbarButton>
</ToolbarGroup>
<ToolbarGroup>
<TurnIntoToolbarButton />
<MarkToolbarButton plugin={BoldPlugin} tooltip="Bold (⌘+B)">
<BoldIcon />
</MarkToolbarButton>
<MarkToolbarButton plugin={ItalicPlugin} tooltip="Italic (⌘+I)">
<ItalicIcon />
</MarkToolbarButton>
<MarkToolbarButton
plugin={UnderlinePlugin}
tooltip="Underline (⌘+U)"
>
<UnderlineIcon />
</MarkToolbarButton>
<MarkToolbarButton
plugin={StrikethroughPlugin}
tooltip="Strikethrough (⌘+⇧+M)"
>
<StrikethroughIcon />
</MarkToolbarButton>
<MarkToolbarButton plugin={CodePlugin} tooltip="Code (⌘+E)">
<Code2Icon />
</MarkToolbarButton>
<InlineEquationToolbarButton />
<LinkToolbarButton />
</ToolbarGroup>
</>
)}
<ToolbarGroup>
<CommentToolbarButton />
<SuggestionToolbarButton />
{!readOnly && <MoreToolbarButton />}
</ToolbarGroup>
</>
);
}
type FloatingToolbarOptions = {
floatingOptions?: UseVirtualFloatingOptions;
hideToolbar?: boolean;
showWhenReadOnly?: boolean;
};
type FloatingToolbarProps = React.ComponentProps<typeof Toolbar> & {
options?: FloatingToolbarOptions;
};
export function FloatingToolbar(props: FloatingToolbarProps) {
const editorId = useEditorId();
const hasNodeSelection = useEditorSelector(
(editor) => editor.read.selection.nodes().length > 0,
{ id: editorId }
);
if (hasNodeSelection) return null;
return <TextFloatingToolbar {...props} />;
}
function TextFloatingToolbar({
children,
className,
options,
...props
}: FloatingToolbarProps) {
const editorId = useEditorId();
const focusedEditorId = useEventEditorValue('focus');
const isFloatingLinkOpen = !!usePluginStore(linkPlugin, 'mode');
const isAIChatOpen = usePluginStore(AIChatPlugin, 'open');
const editor = useEditor({ id: editorId });
const selectionExpanded = useEditorSelector(
(innerEditor) => innerEditor.read.selection.isExpanded(),
{ id: editorId }
);
const selectionText = useEditorSelector(
(innerEditor2) => innerEditor2.read.text.string(),
{ id: editorId }
);
const selectionRange = useEditorSelector(
(innerEditor3) => innerEditor3.read.selection(),
{ id: editorId }
);
const waitForCollapsedSelection = useEditorSelector(
(innerEditor4, previous = false) => {
if (!innerEditor4.read.selection.isExpanded()) return false;
if (editorId !== focusedEditorId) return true;
return previous;
},
{ id: editorId }
);
const readOnly = useEditorReadOnly();
const [dismissedSelection, setDismissedSelection] =
React.useState<typeof selectionRange>(null);
const [mouseDownOpen, setMouseDownOpen] = React.useState<boolean | null>(
null
);
const [ownedOverlayOpen, setOwnedOverlayOpen] = React.useState(false);
const open =
selectionExpanded &&
!!selectionText &&
(editorId === focusedEditorId || ownedOverlayOpen) &&
!isFloatingLinkOpen &&
!isAIChatOpen &&
!options?.hideToolbar &&
(!readOnly || !!options?.showWhenReadOnly) &&
(!waitForCollapsedSelection || readOnly || ownedOverlayOpen) &&
mouseDownOpen !== false &&
dismissedSelection !== selectionRange;
const floating = useVirtualFloating(
mergeProps<UseVirtualFloatingOptions>(
{
open,
getBoundingClientRect: () => getSelectionBoundingClientRect(editor),
onOpenChange: (nextOpen) => {
setDismissedSelection(nextOpen ? null : selectionRange);
},
},
{
middleware: [
offset(12),
flip({
fallbackPlacements: [
'top-start',
'top-end',
'bottom-start',
'bottom-end',
],
padding: 12,
}),
],
placement: 'top',
...options?.floatingOptions,
}
)
);
const openStateRef = React.useRef(open);
React.useEffect(() => {
openStateRef.current = open;
}, [open]);
React.useEffect(() => {
const onMouseUp = () => {
setMouseDownOpen(null);
};
const onMouseDown = () => {
setMouseDownOpen(openStateRef.current);
};
document.addEventListener('mouseup', onMouseUp);
document.addEventListener('mousedown', onMouseDown);
return () => {
document.removeEventListener('mouseup', onMouseUp);
document.removeEventListener('mousedown', onMouseDown);
};
}, []);
const editorVersion = useEditorSelector(
(innerEditor5) => innerEditor5.read.lastCommit()?.version ?? 0,
{ id: editorId }
);
const updateFloating = floating.update;
React.useEffect(() => {
if (open) updateFloating?.();
}, [editorVersion, open, updateFloating]);
const clickOutsideRef = useOnClickOutside(
() => {
setDismissedSelection(selectionRange);
},
{ ignoreClass: 'ignore-click-outside/toolbar' }
);
const ref = useComposedRef<HTMLDivElement>(
props.ref,
floating.refs.setFloating
);
if (!open) return null;
return (
<div ref={clickOutsideRef}>
<Toolbar
{...props}
ref={ref}
onOverlayOpenChange={setOwnedOverlayOpen}
style={floating.style}
className={cn(
'scrollbar-hide absolute z-50 overflow-x-auto whitespace-nowrap rounded-md border bg-popover p-1 opacity-100 shadow-md print:hidden',
'max-w-[80vw]',
className
)}
>
{children}
</Toolbar>
</div>
);
}
export const FloatingToolbarPlugin = definePlatePlugin('floatingToolbar', {
render: {
afterEditable: () => (
<FloatingToolbar>
<FloatingToolbarButtons />
</FloatingToolbar>
),
},
});
export const FloatingToolbarKit = [FloatingToolbarPlugin] as const;'use client';
import {
BoldPlugin,
CodePlugin,
HighlightPlugin,
ItalicPlugin,
StrikethroughPlugin,
UnderlinePlugin,
} from '@platejs/basic-nodes/react';
import {
FontBackgroundColorPlugin,
FontColorPlugin,
} from '@platejs/basic-styles/react';
import {
BaseAudioPlugin,
BaseFilePlugin,
BaseImagePlugin,
BaseVideoPlugin,
} from '@platejs/media';
import {
BaselineIcon,
BoldIcon,
Code2Icon,
HighlighterIcon,
ItalicIcon,
PaintBucketIcon,
StrikethroughIcon,
UnderlineIcon,
WandSparklesIcon,
} from 'lucide-react';
import { useEditorReadOnly, definePlatePlugin } from 'platejs/react';
import * as React from 'react';
import { cn } from '@/lib/utils';
import { ToolbarGroup, Toolbar } from '@/components/editor/toolbar';
import { AIToolbarButton } from './ai-toolbar-button';
import { AlignToolbarButton } from './align-toolbar-button';
import { CommentToolbarButton } from './comment-toolbar-button';
import { EmojiToolbarButton } from './emoji-toolbar-button';
import {
DEFAULT_COLORS,
FontColorToolbarButton,
} from './font-color-toolbar-button';
import { FontSizeToolbarButton } from './font-size-toolbar-button';
import { RedoToolbarButton, UndoToolbarButton } from './history-toolbar-button';
import {
IndentToolbarButton,
OutdentToolbarButton,
} from './indent-toolbar-button';
import { InsertToolbarButton } from './insert-toolbar-button';
import { LineHeightToolbarButton } from './line-height-toolbar-button';
import { LinkToolbarButton } from './link-toolbar-button';
import {
BulletedListToolbarButton,
NumberedListToolbarButton,
TodoListToolbarButton,
} from './list-toolbar-button';
import { MarkToolbarButton } from './mark-toolbar-button';
import { MediaToolbarButton } from './media-toolbar-button';
import { ModeToolbarButton } from './mode-toolbar-button';
import { MoreToolbarButton } from './more-toolbar-button';
import { TableToolbarButton } from './table-toolbar-button';
import { ToggleToolbarButton } from './toggle-toolbar-button';
import { TurnIntoToolbarButton } from './turn-into-toolbar-button';
export function FixedToolbarButtons({
children,
}: {
children?: React.ReactNode;
} = {}) {
const readOnly = useEditorReadOnly();
return (
<div className="flex w-full">
{!readOnly && (
<>
<ToolbarGroup>
<UndoToolbarButton />
<RedoToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<AIToolbarButton tooltip="AI commands">
<WandSparklesIcon />
</AIToolbarButton>
</ToolbarGroup>
{children}
<ToolbarGroup>
<InsertToolbarButton />
<TurnIntoToolbarButton />
<FontSizeToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<MarkToolbarButton plugin={BoldPlugin} tooltip="Bold (⌘+B)">
<BoldIcon />
</MarkToolbarButton>
<MarkToolbarButton plugin={ItalicPlugin} tooltip="Italic (⌘+I)">
<ItalicIcon />
</MarkToolbarButton>
<MarkToolbarButton
plugin={UnderlinePlugin}
tooltip="Underline (⌘+U)"
>
<UnderlineIcon />
</MarkToolbarButton>
<MarkToolbarButton
plugin={StrikethroughPlugin}
tooltip="Strikethrough (⌘+⇧+M)"
>
<StrikethroughIcon />
</MarkToolbarButton>
<MarkToolbarButton plugin={CodePlugin} tooltip="Code (⌘+E)">
<Code2Icon />
</MarkToolbarButton>
<FontColorToolbarButton
colors={DEFAULT_COLORS}
plugin={FontColorPlugin}
tooltip="Text color"
>
<BaselineIcon />
</FontColorToolbarButton>
<FontColorToolbarButton
colors={DEFAULT_COLORS}
plugin={FontBackgroundColorPlugin}
tooltip="Background color"
>
<PaintBucketIcon />
</FontColorToolbarButton>
</ToolbarGroup>
<ToolbarGroup>
<AlignToolbarButton />
<NumberedListToolbarButton />
<BulletedListToolbarButton />
<TodoListToolbarButton />
<ToggleToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<LinkToolbarButton />
<TableToolbarButton />
<EmojiToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<MediaToolbarButton plugin={BaseImagePlugin} />
<MediaToolbarButton plugin={BaseVideoPlugin} />
<MediaToolbarButton plugin={BaseAudioPlugin} />
<MediaToolbarButton plugin={BaseFilePlugin} />
</ToolbarGroup>
<ToolbarGroup>
<LineHeightToolbarButton />
<OutdentToolbarButton />
<IndentToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<MoreToolbarButton />
</ToolbarGroup>
</>
)}
<div className="grow" />
<ToolbarGroup>
<MarkToolbarButton plugin={HighlightPlugin} tooltip="Highlight">
<HighlighterIcon />
</MarkToolbarButton>
<CommentToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<ModeToolbarButton />
</ToolbarGroup>
</div>
);
}
export function FixedToolbar(props: React.ComponentProps<typeof Toolbar>) {
return (
<Toolbar
{...props}
className={cn(
'scrollbar-hide sticky top-0 left-0 z-50 w-full justify-between overflow-x-auto rounded-t-lg border-b border-b-border bg-background/95 p-1 backdrop-blur-sm supports-backdrop-blur:bg-background/60',
props.className
)}
/>
);
}
export const FixedToolbarPlugin = definePlatePlugin('fixedToolbar', {
render: {
beforeEditable: () => (
<FixedToolbar>
<FixedToolbarButtons />
</FixedToolbar>
),
},
});
export const FixedToolbarKit = [FixedToolbarPlugin] as const;'use client';
import { AIChatPlugin } from '@platejs/ai/react';
import {
BoldPlugin,
CodePlugin,
ItalicPlugin,
StrikethroughPlugin,
UnderlinePlugin,
} from '@platejs/basic-nodes/react';
import {
type UseVirtualFloatingOptions,
flip,
getSelectionBoundingClientRect,
offset,
useVirtualFloating,
} from '@platejs/floating';
import { useComposedRef } from '@udecode/cn';
import { useOnClickOutside } from '@udecode/react-utils';
import { mergeProps } from '@udecode/utils';
import {
BoldIcon,
Code2Icon,
ItalicIcon,
StrikethroughIcon,
UnderlineIcon,
WandSparklesIcon,
} from 'lucide-react';
import {
useEditorReadOnly,
definePlatePlugin,
useEditor,
useEditorId,
useEditorSelector,
useEventEditorValue,
usePluginStore,
} from 'platejs/react';
import * as React from 'react';
import { cn } from '@/lib/utils';
import { ToolbarGroup, Toolbar } from '@/components/editor/toolbar';
import { AIToolbarButton } from './ai-toolbar-button';
import { CommentToolbarButton } from './comment-toolbar-button';
import { InlineEquationToolbarButton } from './equation-toolbar-button';
import { linkPlugin } from './link';
import { LinkToolbarButton } from './link-toolbar-button';
import { MarkToolbarButton } from './mark-toolbar-button';
import { MoreToolbarButton } from './more-toolbar-button';
import { SuggestionToolbarButton } from './suggestion-toolbar-button';
import { TurnIntoToolbarButton } from './turn-into-toolbar-button';
export function FloatingToolbarButtons() {
const readOnly = useEditorReadOnly();
return (
<>
{!readOnly && (
<>
<ToolbarGroup>
<AIToolbarButton tooltip="AI commands">
<WandSparklesIcon />
Ask AI
</AIToolbarButton>
</ToolbarGroup>
<ToolbarGroup>
<TurnIntoToolbarButton />
<MarkToolbarButton plugin={BoldPlugin} tooltip="Bold (⌘+B)">
<BoldIcon />
</MarkToolbarButton>
<MarkToolbarButton plugin={ItalicPlugin} tooltip="Italic (⌘+I)">
<ItalicIcon />
</MarkToolbarButton>
<MarkToolbarButton
plugin={UnderlinePlugin}
tooltip="Underline (⌘+U)"
>
<UnderlineIcon />
</MarkToolbarButton>
<MarkToolbarButton
plugin={StrikethroughPlugin}
tooltip="Strikethrough (⌘+⇧+M)"
>
<StrikethroughIcon />
</MarkToolbarButton>
<MarkToolbarButton plugin={CodePlugin} tooltip="Code (⌘+E)">
<Code2Icon />
</MarkToolbarButton>
<InlineEquationToolbarButton />
<LinkToolbarButton />
</ToolbarGroup>
</>
)}
<ToolbarGroup>
<CommentToolbarButton />
<SuggestionToolbarButton />
{!readOnly && <MoreToolbarButton />}
</ToolbarGroup>
</>
);
}
type FloatingToolbarOptions = {
floatingOptions?: UseVirtualFloatingOptions;
hideToolbar?: boolean;
showWhenReadOnly?: boolean;
};
type FloatingToolbarProps = React.ComponentProps<typeof Toolbar> & {
options?: FloatingToolbarOptions;
};
export function FloatingToolbar(props: FloatingToolbarProps) {
const editorId = useEditorId();
const hasNodeSelection = useEditorSelector(
(editor) => editor.read.selection.nodes().length > 0,
{ id: editorId }
);
if (hasNodeSelection) return null;
return <TextFloatingToolbar {...props} />;
}
function TextFloatingToolbar({
children,
className,
options,
...props
}: FloatingToolbarProps) {
const editorId = useEditorId();
const focusedEditorId = useEventEditorValue('focus');
const isFloatingLinkOpen = !!usePluginStore(linkPlugin, 'mode');
const isAIChatOpen = usePluginStore(AIChatPlugin, 'open');
const editor = useEditor({ id: editorId });
const selectionExpanded = useEditorSelector(
(innerEditor) => innerEditor.read.selection.isExpanded(),
{ id: editorId }
);
const selectionText = useEditorSelector(
(innerEditor2) => innerEditor2.read.text.string(),
{ id: editorId }
);
const selectionRange = useEditorSelector(
(innerEditor3) => innerEditor3.read.selection(),
{ id: editorId }
);
const waitForCollapsedSelection = useEditorSelector(
(innerEditor4, previous = false) => {
if (!innerEditor4.read.selection.isExpanded()) return false;
if (editorId !== focusedEditorId) return true;
return previous;
},
{ id: editorId }
);
const readOnly = useEditorReadOnly();
const [dismissedSelection, setDismissedSelection] =
React.useState<typeof selectionRange>(null);
const [mouseDownOpen, setMouseDownOpen] = React.useState<boolean | null>(
null
);
const [ownedOverlayOpen, setOwnedOverlayOpen] = React.useState(false);
const open =
selectionExpanded &&
!!selectionText &&
(editorId === focusedEditorId || ownedOverlayOpen) &&
!isFloatingLinkOpen &&
!isAIChatOpen &&
!options?.hideToolbar &&
(!readOnly || !!options?.showWhenReadOnly) &&
(!waitForCollapsedSelection || readOnly || ownedOverlayOpen) &&
mouseDownOpen !== false &&
dismissedSelection !== selectionRange;
const floating = useVirtualFloating(
mergeProps<UseVirtualFloatingOptions>(
{
open,
getBoundingClientRect: () => getSelectionBoundingClientRect(editor),
onOpenChange: (nextOpen) => {
setDismissedSelection(nextOpen ? null : selectionRange);
},
},
{
middleware: [
offset(12),
flip({
fallbackPlacements: [
'top-start',
'top-end',
'bottom-start',
'bottom-end',
],
padding: 12,
}),
],
placement: 'top',
...options?.floatingOptions,
}
)
);
const openStateRef = React.useRef(open);
React.useEffect(() => {
openStateRef.current = open;
}, [open]);
React.useEffect(() => {
const onMouseUp = () => {
setMouseDownOpen(null);
};
const onMouseDown = () => {
setMouseDownOpen(openStateRef.current);
};
document.addEventListener('mouseup', onMouseUp);
document.addEventListener('mousedown', onMouseDown);
return () => {
document.removeEventListener('mouseup', onMouseUp);
document.removeEventListener('mousedown', onMouseDown);
};
}, []);
const editorVersion = useEditorSelector(
(innerEditor5) => innerEditor5.read.lastCommit()?.version ?? 0,
{ id: editorId }
);
const updateFloating = floating.update;
React.useEffect(() => {
if (open) updateFloating?.();
}, [editorVersion, open, updateFloating]);
const clickOutsideRef = useOnClickOutside(
() => {
setDismissedSelection(selectionRange);
},
{ ignoreClass: 'ignore-click-outside/toolbar' }
);
const ref = useComposedRef<HTMLDivElement>(
props.ref,
floating.refs.setFloating
);
if (!open) return null;
return (
<div ref={clickOutsideRef}>
<Toolbar
{...props}
ref={ref}
onOverlayOpenChange={setOwnedOverlayOpen}
style={floating.style}
className={cn(
'scrollbar-hide absolute z-50 overflow-x-auto whitespace-nowrap rounded-md border bg-popover p-1 opacity-100 shadow-md print:hidden',
'max-w-[80vw]',
className
)}
>
{children}
</Toolbar>
</div>
);
}
export const FloatingToolbarPlugin = definePlatePlugin('floatingToolbar', {
render: {
afterEditable: () => (
<FloatingToolbar>
<FloatingToolbarButtons />
</FloatingToolbar>
),
},
});
export const FloatingToolbarKit = [FloatingToolbarPlugin] as const;import { useEditor } from 'platejs/react';
import { CustomIcon } from 'lucide-react';
import { ToolbarButton } from '@/components/editor/toolbar';
export function CustomToolbarButton() {
const editor = useEditor();
return (
<ToolbarButton
onClick={() => {
// Custom action
editor.update((tx) => {
tx.text.insert('Custom text');
});
}}
tooltip="Custom Action"
>
<CustomIcon />
</ToolbarButton>
);
}'use client';
import { ElementApi } from '@platejs/plite';
import { PLUGINS } from '@platejs/utils';
import {
ChevronRightIcon,
Code2Icon,
Columns3Icon,
FileCodeIcon,
Heading1Icon,
Heading2Icon,
Heading3Icon,
Heading4Icon,
Heading5Icon,
Heading6Icon,
ListIcon,
ListOrderedIcon,
PilcrowIcon,
QuoteIcon,
SquareIcon,
} from 'lucide-react';
import { useEditor, useSelectionFragmentProp } from 'platejs/react';
import * as React from 'react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from '@/components/editor/dropdown-menu';
import {
ToolbarButton,
ToolbarMenuGroup,
} from '@/components/editor/toolbar';
import {
getBlockType,
applyBlockAction,
} from '@/components/editor/transforms';
const ACTION_THREE_COLUMNS = 'action_three_columns';
function getTurnIntoLabel(value: string) {
switch (value) {
case 'disc': {
return 'Bulleted list';
}
case 'decimal': {
return 'Numbered list';
}
case 'todo': {
return 'To-do list';
}
case 'heading-1': {
return 'Heading 1';
}
case 'heading-2': {
return 'Heading 2';
}
case 'heading-3': {
return 'Heading 3';
}
case 'heading-4': {
return 'Heading 4';
}
case 'heading-5': {
return 'Heading 5';
}
case 'heading-6': {
return 'Heading 6';
}
case PLUGINS.toggle: {
return 'Toggle list';
}
case PLUGINS.codeBlock: {
return 'Code';
}
case PLUGINS.codeDrawing: {
return 'Code Drawing';
}
case PLUGINS.blockquote: {
return 'Quote';
}
case ACTION_THREE_COLUMNS: {
return '3 columns';
}
default: {
return 'Text';
}
}
}
function TurnIntoMenuItem({
children,
icon,
value,
}: React.PropsWithChildren<{
icon: React.ReactNode;
value: string;
}>) {
return (
<DropdownMenuRadioItem
className="min-w-[180px] pl-2 *:first:[span]:hidden"
value={value}
>
{icon}
{children}
</DropdownMenuRadioItem>
);
}
export function TurnIntoToolbarButton() {
const editor = useEditor();
const [open, setOpen] = React.useState(false);
const documentValue = useSelectionFragmentProp({
defaultValue: editor.plugin(PLUGINS.paragraph).schema.type,
getProp: (node) =>
ElementApi.isElement(node) ? getBlockType(node) : undefined,
});
const selectedDocumentValue =
typeof documentValue === 'string' ? documentValue : PLUGINS.paragraph;
const selectedValue =
[
PLUGINS.paragraph,
'heading-1',
'heading-2',
'heading-3',
'heading-4',
'heading-5',
'heading-6',
PLUGINS.toggle,
PLUGINS.codeBlock,
PLUGINS.codeDrawing,
PLUGINS.blockquote,
].find((name) => name === selectedDocumentValue) ?? selectedDocumentValue;
return (
<DropdownMenu open={open} onOpenChange={setOpen} modal={false}>
<DropdownMenuTrigger>
<ToolbarButton
className="min-w-[125px]"
pressed={open}
tooltip="Turn into"
isDropdown
>
{getTurnIntoLabel(selectedValue)}
</ToolbarButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="ignore-click-outside/toolbar min-w-0"
onFinalFocus={(event) => {
event.preventDefault();
editor.api.dom.focus();
}}
align="start"
>
<DropdownMenuRadioGroup
value={selectedValue}
onValueChange={(action) => {
applyBlockAction(editor, action);
}}
>
<ToolbarMenuGroup label="Turn into">
<TurnIntoMenuItem icon={<PilcrowIcon />} value={PLUGINS.paragraph}>
Text
</TurnIntoMenuItem>
<TurnIntoMenuItem icon={<Heading1Icon />} value="heading-1">
Heading 1
</TurnIntoMenuItem>
<TurnIntoMenuItem icon={<Heading2Icon />} value="heading-2">
Heading 2
</TurnIntoMenuItem>
<TurnIntoMenuItem icon={<Heading3Icon />} value="heading-3">
Heading 3
</TurnIntoMenuItem>
<TurnIntoMenuItem icon={<Heading4Icon />} value="heading-4">
Heading 4
</TurnIntoMenuItem>
<TurnIntoMenuItem icon={<Heading5Icon />} value="heading-5">
Heading 5
</TurnIntoMenuItem>
<TurnIntoMenuItem icon={<Heading6Icon />} value="heading-6">
Heading 6
</TurnIntoMenuItem>
<TurnIntoMenuItem icon={<ListIcon />} value="disc">
Bulleted list
</TurnIntoMenuItem>
<TurnIntoMenuItem icon={<ListOrderedIcon />} value="decimal">
Numbered list
</TurnIntoMenuItem>
<TurnIntoMenuItem icon={<SquareIcon />} value="todo">
To-do list
</TurnIntoMenuItem>
<TurnIntoMenuItem
icon={<ChevronRightIcon />}
value={PLUGINS.toggle}
>
Toggle list
</TurnIntoMenuItem>
<TurnIntoMenuItem icon={<FileCodeIcon />} value={PLUGINS.codeBlock}>
Code
</TurnIntoMenuItem>
<TurnIntoMenuItem icon={<Code2Icon />} value={PLUGINS.codeDrawing}>
Code Drawing
</TurnIntoMenuItem>
<TurnIntoMenuItem icon={<QuoteIcon />} value={PLUGINS.blockquote}>
Quote
</TurnIntoMenuItem>
<TurnIntoMenuItem
icon={<Columns3Icon />}
value={ACTION_THREE_COLUMNS}
>
3 columns
</TurnIntoMenuItem>
</ToolbarMenuGroup>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
);
}'use client';
import {
CalendarIcon,
ChevronRightIcon,
Code2,
Columns3Icon,
FileCodeIcon,
FilmIcon,
Heading1Icon,
Heading2Icon,
Heading3Icon,
ImageIcon,
Link2Icon,
ListIcon,
ListOrderedIcon,
MinusIcon,
PenToolIcon,
PilcrowIcon,
PlusIcon,
QuoteIcon,
RadicalIcon,
SquareIcon,
SuperscriptIcon,
TableIcon,
TableOfContentsIcon,
} from 'lucide-react';
import { PLUGINS } from 'platejs';
import { type PlateEditor, useEditor } from 'platejs/react';
import * as React from 'react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
ToolbarButton,
ToolbarMenuGroup,
} from '@/components/editor/toolbar';
import {
insertBlock,
insertInlineElement,
} from '@/components/editor/transforms';
type Group = {
group: string;
items: Item[];
};
type Item = {
icon: React.ReactNode;
value: string;
onSelect: (editor: PlateEditor, value: string) => void;
focusEditor?: boolean;
label?: string;
};
const groups: Group[] = [
{
group: 'Basic blocks',
items: [
{
icon: <PilcrowIcon />,
label: 'Paragraph',
value: PLUGINS.paragraph,
},
{
icon: <Heading1Icon />,
label: 'Heading 1',
value: 'heading-1',
},
{
icon: <Heading2Icon />,
label: 'Heading 2',
value: 'heading-2',
},
{
icon: <Heading3Icon />,
label: 'Heading 3',
value: 'heading-3',
},
{
icon: <TableIcon />,
label: 'Table',
value: PLUGINS.table,
},
{
icon: <FileCodeIcon />,
label: 'Code',
value: PLUGINS.codeBlock,
},
{
icon: <QuoteIcon />,
label: 'Quote',
value: PLUGINS.blockquote,
},
{
icon: <MinusIcon />,
label: 'Divider',
value: PLUGINS.horizontalRule,
},
].map((item) => ({
...item,
onSelect: (editor, value) => {
insertBlock(editor, value);
},
})),
},
{
group: 'Lists',
items: [
{
icon: <ListIcon />,
label: 'Bulleted list',
value: 'disc',
},
{
icon: <ListOrderedIcon />,
label: 'Numbered list',
value: 'decimal',
},
{
icon: <SquareIcon />,
label: 'To-do list',
value: 'todo',
},
{
icon: <ChevronRightIcon />,
label: 'Toggle list',
value: PLUGINS.toggle,
},
].map((item) => ({
...item,
onSelect: (editor, value) => {
insertBlock(editor, value);
},
})),
},
{
group: 'Media',
items: [
{
icon: <ImageIcon />,
label: 'Image',
value: PLUGINS.image,
},
{
icon: <FilmIcon />,
label: 'Embed',
value: PLUGINS.mediaEmbed,
},
].map((item) => ({
...item,
onSelect: (editor, value) => {
insertBlock(editor, value);
},
})),
},
{
group: 'Advanced blocks',
items: [
{
icon: <TableOfContentsIcon />,
label: 'Table of contents',
value: PLUGINS.toc,
},
{
icon: <Columns3Icon />,
label: '3 columns',
value: 'action_three_columns',
},
{
focusEditor: false,
icon: <RadicalIcon />,
label: 'Equation',
value: PLUGINS.equation,
},
{
icon: <PenToolIcon />,
label: 'Excalidraw',
value: PLUGINS.excalidraw,
},
{
icon: <Code2 />,
label: 'Code Drawing',
value: PLUGINS.codeDrawing,
},
].map((item) => ({
...item,
onSelect: (editor, value) => {
insertBlock(editor, value);
},
})),
},
{
group: 'Inline',
items: [
{
icon: <Link2Icon />,
label: 'Link',
value: PLUGINS.link,
},
{
focusEditor: true,
icon: <CalendarIcon />,
label: 'Date',
value: PLUGINS.date,
},
{
focusEditor: true,
icon: <SuperscriptIcon />,
label: 'Footnote',
value: 'action_footnote',
},
{
focusEditor: false,
icon: <RadicalIcon />,
label: 'Inline Equation',
value: PLUGINS.inlineEquation,
},
].map((item) => ({
...item,
onSelect: (editor, value) => {
insertInlineElement(editor, value);
},
})),
},
];
export function InsertToolbarButton() {
const editor = useEditor();
const [open, setOpen] = React.useState(false);
return (
<DropdownMenu open={open} onOpenChange={setOpen} modal={false}>
<DropdownMenuTrigger asChild>
<ToolbarButton pressed={open} tooltip="Insert" isDropdown>
<PlusIcon />
</ToolbarButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="flex max-h-[500px] min-w-0 flex-col overflow-y-auto"
align="start"
>
{groups.map(({ group, items: nestedItems }) => (
<ToolbarMenuGroup key={group} label={group}>
{nestedItems.map(({ icon, label, value, onSelect }) => (
<DropdownMenuItem
key={value}
className="min-w-[180px]"
onSelect={() => {
onSelect(editor, value);
editor.api.dom.focus();
}}
>
{icon}
{label}
</DropdownMenuItem>
))}
</ToolbarMenuGroup>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}{
group: 'Basic blocks',
items: [
// ... existing items
{
icon: <CustomIcon />,
label: 'Custom Block',
value: 'custom-block',
},
].map((item) => ({
...item,
onSelect: (editor, value) => {
insertBlock(editor, value);
},
})),
}