Documentation Index
Fetch the complete documentation index at: https://mintlify.com/prosekit/prosekit/llms.txt
Use this file to discover all available pages before exploring further.
The Inline Menu component displays a floating toolbar when users select text in the editor. It provides quick access to formatting options like bold, italic, links, and other inline commands.
Features
- Automatically appears on text selection
- Positions intelligently around selected text
- Keyboard shortcuts support
- Customizable content
- Handles viewport boundaries
- Multiple popovers support (e.g., separate link editor)
Installation
import { InlinePopover } from 'prosekit/react/inline-popover'
Basic Usage
import { useEditor } from 'prosekit/react'
import { InlinePopover } from 'prosekit/react/inline-popover'
export default function InlineMenu() {
const editor = useEditor()
return (
<InlinePopover className="inline-menu">
<button onClick={() => editor.commands.toggleBold()}>
Bold
</button>
<button onClick={() => editor.commands.toggleItalic()}>
Italic
</button>
<button onClick={() => editor.commands.toggleUnderline()}>
Underline
</button>
</InlinePopover>
)
}
Component Props
Where to position the popover relative to the selection. Options include:
top, bottom, left, right
- With modifiers:
top-start, top-end, bottom-start, bottom-end
Distance in pixels between the popover and the selected text.
Whether the popover should automatically open when text is selected. Set to false if you want manual control.
Manually control whether the popover is open. Note: The popover will still hide if selection is empty.
Whether pressing Escape should dismiss the popover.
Callback when the open state changes.
CSS class name for styling.
Complete Example
Here’s a full inline menu with formatting options and link support:
import { useEditor, useEditorDerivedValue } from 'prosekit/react'
import { InlinePopover } from 'prosekit/react/inline-popover'
import { useState } from 'react'
function getMenuState(editor: Editor) {
return {
bold: {
isActive: editor.marks.bold.isActive(),
canExec: editor.commands.toggleBold.canExec(),
},
italic: {
isActive: editor.marks.italic.isActive(),
canExec: editor.commands.toggleItalic.canExec(),
},
underline: {
isActive: editor.marks.underline.isActive(),
canExec: editor.commands.toggleUnderline.canExec(),
},
strike: {
isActive: editor.marks.strike.isActive(),
canExec: editor.commands.toggleStrike.canExec(),
},
code: {
isActive: editor.marks.code.isActive(),
canExec: editor.commands.toggleCode.canExec(),
},
link: {
isActive: editor.marks.link.isActive(),
canExec: editor.commands.addLink.canExec({ href: '' }),
},
}
}
export default function InlineMenu() {
const editor = useEditor()
const state = useEditorDerivedValue(getMenuState)
const [linkMenuOpen, setLinkMenuOpen] = useState(false)
const handleLinkSubmit = (href: string) => {
if (href) {
editor.commands.addLink({ href })
} else {
editor.commands.removeLink()
}
setLinkMenuOpen(false)
editor.focus()
}
return (
<>
{/* Main formatting menu */}
<InlinePopover
className="inline-menu"
onOpenChange={(open) => {
if (!open) setLinkMenuOpen(false)
}}
>
<button
disabled={!state.bold.canExec}
data-active={state.bold.isActive}
onClick={() => editor.commands.toggleBold()}
>
Bold
</button>
<button
disabled={!state.italic.canExec}
data-active={state.italic.isActive}
onClick={() => editor.commands.toggleItalic()}
>
Italic
</button>
<button
disabled={!state.underline.canExec}
data-active={state.underline.isActive}
onClick={() => editor.commands.toggleUnderline()}
>
Underline
</button>
<button
disabled={!state.strike.canExec}
data-active={state.strike.isActive}
onClick={() => editor.commands.toggleStrike()}
>
Strike
</button>
<button
disabled={!state.code.canExec}
data-active={state.code.isActive}
onClick={() => editor.commands.toggleCode()}
>
Code
</button>
{state.link.canExec && (
<button
data-active={state.link.isActive}
onClick={() => {
editor.commands.expandLink()
setLinkMenuOpen(true)
}}
>
Link
</button>
)}
</InlinePopover>
{/* Separate link editor */}
<InlinePopover
placement="bottom"
defaultOpen={false}
open={linkMenuOpen}
onOpenChange={setLinkMenuOpen}
className="inline-menu-link"
>
{linkMenuOpen && (
<form onSubmit={(e) => {
e.preventDefault()
const href = new FormData(e.target).get('href')
handleLinkSubmit(href)
}}>
<input
name="href"
placeholder="Paste link..."
autoFocus
/>
</form>
)}
{state.link.isActive && (
<button onClick={() => handleLinkSubmit('')}>
Remove link
</button>
)}
</InlinePopover>
</>
)
}
Styling
.inline-menu {
display: flex;
gap: 4px;
padding: 4px;
background: white;
border: 1px solid #e5e7eb;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.inline-menu button {
padding: 6px 12px;
border: none;
background: transparent;
border-radius: 4px;
cursor: pointer;
transition: background 0.15s;
}
.inline-menu button:hover {
background: #f3f4f6;
}
.inline-menu button[data-active="true"] {
background: #e5e7eb;
font-weight: 600;
}
.inline-menu button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.inline-menu-link {
padding: 8px;
background: white;
border: 1px solid #e5e7eb;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.inline-menu-link input {
padding: 6px 12px;
border: 1px solid #e5e7eb;
border-radius: 4px;
outline: none;
width: 300px;
}
.inline-menu-link input:focus {
border-color: #3b82f6;
}
Multiple Popovers
You can render multiple InlinePopover components for different purposes:
<>
{/* Main formatting menu */}
<InlinePopover className="inline-menu-main">
{/* Formatting buttons */}
</InlinePopover>
{/* Link editor */}
<InlinePopover
placement="bottom"
defaultOpen={false}
open={linkMenuOpen}
className="inline-menu-link"
>
{/* Link input */}
</InlinePopover>
{/* Color picker */}
<InlinePopover
defaultOpen={false}
open={colorMenuOpen}
className="inline-menu-color"
>
{/* Color picker */}
</InlinePopover>
</>
Positioning
The inline menu automatically:
- Positions relative to the selected text
- Adjusts position to stay within viewport
- Flips to the opposite side if there’s not enough space
- Hides when selection is empty
Custom Placement
{/* Show below the selection */}
<InlinePopover placement="bottom">
{/* content */}
</InlinePopover>
{/* Show to the right */}
<InlinePopover placement="right-start">
{/* content */}
</InlinePopover>
Behavior Control
Manual Control
Disable automatic opening and control it manually:
const [open, setOpen] = useState(false)
<InlinePopover
defaultOpen={false}
open={open}
onOpenChange={setOpen}
>
{/* content */}
</InlinePopover>
Always Open
Keep the menu open even when selection is empty (not recommended):
<InlinePopover open={true}>
{/* content */}
</InlinePopover>
Accessibility
- The inline menu is keyboard accessible
- Pressing Escape dismisses the menu (configurable)
- Focus is managed automatically
- ARIA attributes are included
Best Practices
-
Keep it simple: Don’t overcrowd the inline menu. Include only the most common formatting options
-
Visual feedback: Show active states for applied formatting
-
Disable unavailable actions: Disable buttons when commands can’t be executed
-
Responsive positioning: The default positioning works well, but test in your layout
-
Multiple menus: Use separate popovers for complex UI like link editing or color picking
-
Mobile considerations: On mobile, the inline menu might overlap with the selection handles. Consider using a fixed position toolbar instead
Common Issues
This can happen if you’re computing expensive values on every render. Use useEditorDerivedValue to optimize:
const state = useEditorDerivedValue(getMenuState)
Make sure you’re using the editor hooks properly and wrapping your editor in the ProseKit provider.
See Also