AI Menu
A fully-featured AI-powered contextual menu for Tiptap editors. Provides intelligent content editing, generation, and transformation capabilities with floating menu positioning and customizable AI actions.
Installation
Add the component via the Tiptap CLI:
npx @tiptap/cli@latest add ai-menuComponents
<AiMenu />
A comprehensive AI-powered menu that provides contextual editing and generation capabilities.
Usage
import { EditorContent, EditorContext, useEditor } from '@tiptap/react'
// --- Tiptap Core Extensions ---
import { StarterKit } from '@tiptap/starter-kit'
import { Ai } from '@tiptap-pro/extension-ai'
import { UiState } from '@/components/tiptap-extension/ui-state-extension'
import { HorizontalRule } from '@/components/tiptap-node/horizontal-rule-node/horizontal-rule-node-extension'
import { Selection } from '@tiptap/extensions'
import { AiProvider, useAi } from '@/components/contexts/ai-context'
// --- Tiptap UI ---
import { AiMenu } from '@/components/tiptap-ui/ai-menu'
import { AiAskButton } from '@/components/tiptap-ui/ai-ask-button'
// --- UI Primitive ---
import { ButtonGroup } from '@/components/tiptap-ui-primitive/button'
// --- Utils ---
import { TIPTAP_AI_APP_ID } from '@/lib/tiptap-collab-utils'
// --- Tiptap Node ---
import '@/components/tiptap-node/blockquote-node/blockquote-node.scss'
import '@/components/tiptap-node/code-block-node/code-block-node.scss'
import '@/components/tiptap-node/horizontal-rule-node/horizontal-rule-node.scss'
import '@/components/tiptap-node/heading-node/heading-node.scss'
import '@/components/tiptap-node/paragraph-node/paragraph-node.scss'
export const AiMenuExample = () => {
return (
<AiProvider>
<AiEditorWrapper />
</AiProvider>
)
}
const AiEditorWrapper = () => {
const { aiToken } = useAi()
if (!aiToken) {
return <div className="tiptap-editor-wrapper">Loading AI...</div>
}
return <AiEditor aiToken={aiToken} />
}
const AiEditor = ({ aiToken }: { aiToken: string }) => {
const editor = useEditor({
immediatelyRender: false,
extensions: [
StarterKit.configure({
horizontalRule: false,
}),
HorizontalRule,
Selection,
UiState,
Ai.configure({
appId: TIPTAP_AI_APP_ID,
token: aiToken,
autocompletion: false,
showDecorations: true,
hideDecorationsOnStreamEnd: false,
onLoading: (context) => {
context.editor.commands.aiGenerationSetIsLoading(true)
context.editor.commands.aiGenerationHasMessage(false)
},
onChunk: (context) => {
context.editor.commands.aiGenerationSetIsLoading(true)
context.editor.commands.aiGenerationHasMessage(true)
},
onSuccess: (context) => {
const hasMessage = !!context.response
context.editor.commands.aiGenerationSetIsLoading(false)
context.editor.commands.aiGenerationHasMessage(hasMessage)
},
}),
],
content: `
<p>Today, we're exploring how AI is transforming creative workflows. From writing assistance to intelligent summarization, the tools at our fingertips are evolving fast. But how do we use them responsibly?</p>
<p>In this article, we'll look at real-world examples of AI enhancing—not replacing—human creativity.</p>
`,
})
return (
<EditorContext.Provider value={{ editor }}>
<div className="controls-bar">
<div className="control-item">
<ButtonGroup orientation="horizontal">
<AiAskButton />
</ButtonGroup>
</div>
</div>
<EditorContent editor={editor} role="presentation" className="control-showcase">
<AiMenu anchorToSelection />
</EditorContent>
</EditorContext.Provider>
)
}Props
| Name | Type | Default | Description |
|---|---|---|---|
editor | Editor | null | undefined | The Tiptap editor instance |
anchorToSelection | boolean | false | When true, anchors the menu to the current text selection spanning the full editor width, with dynamic scroll tracking. |
<AiMenuStateProvider />
State provider that manages AI menu state across the application.
Usage
import { AiMenuStateProvider } from '@/components/tiptap-ui/ai-menu'
function App() {
return <AiMenuStateProvider>{/* Your editor components */}</AiMenuStateProvider>
}<AiMenuContent />
The main content component that renders the AI menu interface.
Props
| Name | Type | Default | Description |
|---|---|---|---|
editor | Editor | null | undefined | The Tiptap editor instance |
anchorToSelection | boolean | false | When true, anchors the menu to the current text selection spanning the full editor width, with dynamic scroll tracking. |
Hooks
useAiMenuState()
Hook to access and manage AI menu state.
Usage
import { useAiMenuState } from '@/components/tiptap-ui/ai-menu'
function MyComponent() {
const { state, updateState, setFallbackAnchor, reset } = useAiMenuState()
const handleOpenMenu = () => {
updateState({ isOpen: true, shouldShowInput: true })
}
const handleCloseMenu = () => {
reset()
}
return (
<button onClick={state.isOpen ? handleCloseMenu : handleOpenMenu}>
{state.isOpen ? 'Close AI Menu' : 'Open AI Menu'}
</button>
)
}Return Values
| Name | Type | Description |
|---|---|---|
state | AiMenuState | Current AI menu state |
updateState | (updates: Partial<AiMenuState>) => void | Function to update menu state |
setFallbackAnchor | (element: HTMLElement | null, rect?) => void | Set fallback positioning anchor |
reset | () => void | Reset menu state to initial state |
useAiContentTracker()
Hook to track AI-generated content changes in the editor. Supports two positioning modes: a streaming mode (when anchorToSelection is true and AI is loading) that uses a ResizeObserver to continuously reposition the anchor as content streams in, and a static mode that creates a scroll-aware virtual anchor at the AI element's position.
Usage
import { useAiContentTracker } from '@/components/tiptap-ui/ai-menu'
function MyComponent() {
const { editor } = useTiptapEditor()
useAiContentTracker({
editor,
aiGenerationActive: true,
setAnchorElement: (el) => store.setAnchorElement(el),
anchorToSelection: true,
})
return <div>Component with AI content tracking</div>
}Parameters
| Name | Type | Default | Description |
|---|---|---|---|
editor | Editor | null | — | The Tiptap editor instance |
aiGenerationActive | boolean | — | Whether AI generation is currently active |
setAnchorElement | (element: HTMLElement) => void | — | Callback to set the floating menu's anchor element |
anchorToSelection | boolean | false | Use editor-width virtual anchors with scroll tracking and streaming support |
useTextSelectionTracker()
Hook to track text selection changes for menu positioning. When anchorToSelection is true, creates editor-width virtual anchors at the selection's vertical position instead of using the raw selected DOM element.
Usage
import { useTextSelectionTracker } from '@/components/tiptap-ui/ai-menu'
function MyComponent() {
const { editor } = useTiptapEditor()
useTextSelectionTracker({
editor,
aiGenerationActive: true,
showMenuAtElement: (el) => show(el),
setMenuVisible: (visible) => updateState({ isOpen: visible }),
anchorToSelection: true,
})
return <div>Component with selection tracking</div>
}Parameters
| Name | Type | Default | Description |
|---|---|---|---|
editor | Editor | null | — | The Tiptap editor instance |
aiGenerationActive | boolean | — | Whether AI generation is currently active |
showMenuAtElement | (element: HTMLElement) => void | — | Callback to show the menu at a given element |
setMenuVisible | (visible: boolean) => void | — | Callback to toggle menu visibility |
onSelectionChange | (element: HTMLElement | null, rect: DOMRect | null) => void | — | Optional callback when selection changes |
prevent | boolean | false | Prevents tracking when true |
anchorToSelection | boolean | false | Use editor-width virtual anchors at the selection's vertical position |
Sub-Components
<AiMenuItems />
Component that renders the available AI actions grouped by category.
Usage
import { AiMenuItems } from '@/components/tiptap-ui/ai-menu'
function CustomAiMenu() {
const { editor } = useTiptapEditor()
return (
<AiMenuItems
editor={editor}
availableActions={['improveWriting', 'aiFixSpellingAndGrammar', 'summarize']}
/>
)
}<AiMenuActions />
Component that renders action buttons for the AI menu (Accept, Regenerate, etc.).
Usage
import { AiMenuActions } from '@/components/tiptap-ui/ai-menu'
function CustomAiMenu() {
const { editor } = useTiptapEditor()
return (
<AiMenuActions
editor={editor}
onAccept={() => console.log('AI content accepted')}
onRegenerate={() => console.log('Regenerating AI content')}
/>
)
}<AiMenuInputTextarea />
Input component for custom AI prompts.
Usage
import { AiMenuInputTextarea } from '@/components/tiptap-ui/ai-menu'
function CustomPromptInput() {
const handleSubmit = (prompt: string) => {
console.log('User prompt:', prompt)
}
return (
<AiMenuInputTextarea
onSubmit={handleSubmit}
placeholder="Ask AI to help with your content..."
/>
)
}Utilities
getContextAndInsertAt(editor)
Utility function to determine the context and insertion point for AI operations.
import { getContextAndInsertAt } from '@/components/tiptap-ui/ai-menu'
const { context, insertAt, isSelection } = getContextAndInsertAt(editor)
if (isSelection) {
// Handle selection-based AI operation
editor.chain().aiEdit({ prompt: 'Improve this text', insertAt }).run()
} else {
// Handle insertion-based AI operation
editor.chain().aiGenerate({ prompt: 'Write about AI', insertAt }).run()
}findPrioritizedAIElement(editor)
Finds the most appropriate DOM element for AI menu positioning.
import { findPrioritizedAIElement } from '@/components/tiptap-ui/ai-menu'
const targetElement = findPrioritizedAIElement(editor)
if (targetElement) {
// Position menu relative to this element
}getSelectionRangeRect(editor)
Gets the bounding rect of the current text selection using the browser's Selection API, with a ProseMirror coordinate fallback. Returns null if the selection is empty.
import { getSelectionRangeRect } from '@/components/tiptap-ui/ai-menu'
const rect = getSelectionRangeRect(editor)
if (rect) {
// Use rect for positioning
}createEditorWidthAnchorRect(editorDom, sourceRect)
Creates a full-width anchor DOMRect spanning the editor's content area (excluding padding and border) at the vertical position of the given source rect.
import { createEditorWidthAnchorRect } from '@/components/tiptap-ui/ai-menu'
const selectionRect = getSelectionRangeRect(editor)
if (selectionRect) {
const anchorRect = createEditorWidthAnchorRect(editor.view.dom, selectionRect)
// anchorRect spans the full editor content width at the selection's vertical position
}createVirtualAnchor(rect, referenceElement?)
Creates a virtual anchor element that reports a bounding rect via getBoundingClientRect(). When a referenceElement is provided, the anchor stores offsets relative to the reference and dynamically recomputes viewport coordinates on each call, enabling scroll-aware positioning.
import { createVirtualAnchor, createEditorWidthAnchorRect } from '@/components/tiptap-ui/ai-menu'
const anchorRect = createEditorWidthAnchorRect(editor.view.dom, selectionRect)
const anchor = createVirtualAnchor(anchorRect, editor.view.dom)
// The anchor's getBoundingClientRect() will update as the editor scrolls
store.setAnchorElement(anchor)getEditorContentRect(editorDom)
Computes the content-area rect of the editor element, excluding padding and border. Returns an object with left and width properties.
import { getEditorContentRect } from '@/components/tiptap-ui/ai-menu'
const { left, width } = getEditorContentRect(editor.view.dom)AI Actions
The AI Menu provides several built-in actions organized by category:
Edit Actions
- Adjust Tone: Change the tone of selected text
- Fix Spelling & Grammar: Correct spelling and grammar errors
- Extend: Expand on the selected content
- Shorten: Make the content more concise
- Simplify Language: Use simpler, clearer language
- Improve Writing: Enhance overall writing quality
- Emojify: Add relevant emojis to the content
Write Actions
- Continue Writing: Generate continuation of the content
- Summarize: Create a summary of the selected text
- Translate To: Translate content to different languages
State Management
AiMenuState Interface
interface AiMenuState {
isOpen: boolean
tone?: string
language: string
shouldShowInput: boolean
inputIsFocused: boolean
fallbackAnchor: {
element: HTMLElement | null
rect: DOMRect | null
}
}State Updates
The AI menu state can be updated using the updateState function:
const { updateState } = useAiMenuState()
// Open menu with input focused
updateState({
isOpen: true,
shouldShowInput: true,
inputIsFocused: true,
})
// Set language for translation
updateState({ language: 'es' })
// Set tone for content adjustment
updateState({ tone: 'professional' })Requirements
Dependencies
@tiptap/react- Core Tiptap React integration@tiptap-pro/extension-ai- AI extension for content generation@tiptap/starter-kit- Basic Tiptap extensionsreact-hotkeys-hook- Keyboard shortcut management
Extensions
ui-state-extension- Manages UI state for AI operationsselection-extension- Enhanced text selection handling
Referenced Components
use-tiptap-editor(hook)use-ui-editor-state(hook)menu(primitive)button,button-group(primitive)card(primitive)combobox(primitive)tiptap-utils(lib)sparkles-icon,stop-circle-2-icon(icons)
Configuration
AI Provider Setup
import { AiProvider } from '@/contexts/ai-context'
function App() {
return (
<AiProvider appId="your-app-id" token="your-ai-token">
<YourEditor />
</AiProvider>
)
}