AI Menu

Available in Start plan

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-menu

Components

<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

NameTypeDefaultDescription
editorEditor | nullundefinedThe Tiptap editor instance
anchorToSelectionbooleanfalseWhen 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

NameTypeDefaultDescription
editorEditor | nullundefinedThe Tiptap editor instance
anchorToSelectionbooleanfalseWhen 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

NameTypeDescription
stateAiMenuStateCurrent AI menu state
updateState(updates: Partial<AiMenuState>) => voidFunction to update menu state
setFallbackAnchor(element: HTMLElement | null, rect?) => voidSet fallback positioning anchor
reset() => voidReset 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

NameTypeDefaultDescription
editorEditor | nullThe Tiptap editor instance
aiGenerationActivebooleanWhether AI generation is currently active
setAnchorElement(element: HTMLElement) => voidCallback to set the floating menu's anchor element
anchorToSelectionbooleanfalseUse 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

NameTypeDefaultDescription
editorEditor | nullThe Tiptap editor instance
aiGenerationActivebooleanWhether AI generation is currently active
showMenuAtElement(element: HTMLElement) => voidCallback to show the menu at a given element
setMenuVisible(visible: boolean) => voidCallback to toggle menu visibility
onSelectionChange(element: HTMLElement | null, rect: DOMRect | null) => voidOptional callback when selection changes
preventbooleanfalsePrevents tracking when true
anchorToSelectionbooleanfalseUse 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 extensions
  • react-hotkeys-hook - Keyboard shortcut management

Extensions

  • ui-state-extension - Manages UI state for AI operations
  • selection-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>
  )
}