Skip to main content

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

placement
string
default:"top"
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
offset
number
default:"12"
Distance in pixels between the popover and the selected text.
defaultOpen
boolean
default:"true"
Whether the popover should automatically open when text is selected. Set to false if you want manual control.
open
boolean
Manually control whether the popover is open. Note: The popover will still hide if selection is empty.
dismissOnEscape
boolean
default:"true"
Whether pressing Escape should dismiss the popover.
onOpenChange
(open: boolean) => void
Callback when the open state changes.
className
string
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

  1. Keep it simple: Don’t overcrowd the inline menu. Include only the most common formatting options
  2. Visual feedback: Show active states for applied formatting
  3. Disable unavailable actions: Disable buttons when commands can’t be executed
  4. Responsive positioning: The default positioning works well, but test in your layout
  5. Multiple menus: Use separate popovers for complex UI like link editing or color picking
  6. 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

Build docs developers (and LLMs) love