Skip to main content

Overview

The Mention extension enables @mention functionality in your editor, allowing you to reference users, tags, or any custom entities. Mentions are rendered as inline nodes with customizable data attributes.

Installation

import { defineMention } from '@prosekit/extensions'

Basic Usage

import { defineMention } from '@prosekit/extensions'
import { createEditor } from '@prosekit/core'

const editor = createEditor({
  extensions: [
    defineMention(),
    // ... other extensions
  ],
})

Mention Attributes

Mentions support three attributes:
interface MentionAttrs {
  /**
   * Unique identifier for the mentioned entity.
   */
  id: string
  
  /**
   * Display value shown in the editor.
   */
  value: string
  
  /**
   * Type or category of the mention (e.g., 'user', 'tag', 'channel').
   */
  kind: string
}

Commands

insertMention

Inserts a mention at the current cursor position.
editor.commands.insertMention({
  id: 'user-123',
  value: '@john',
  kind: 'user',
})
Parameters:
  • id: Unique identifier (required)
  • value: Display text (required)
  • kind: Mention type (required, defaults to empty string)

Usage Examples

User Mentions

// Mention a user
editor.commands.insertMention({
  id: 'user-456',
  value: '@jane-doe',
  kind: 'user',
})

Tag Mentions

// Mention a tag
editor.commands.insertMention({
  id: 'tag-789',
  value: '#important',
  kind: 'tag',
})

Channel Mentions

// Mention a channel
editor.commands.insertMention({
  id: 'channel-101',
  value: '#general',
  kind: 'channel',
})

Custom Entity Types

// Mention a project
editor.commands.insertMention({
  id: 'project-202',
  value: 'Project Alpha',
  kind: 'project',
})

// Mention an issue
editor.commands.insertMention({
  id: 'issue-303',
  value: 'ISSUE-123',
  kind: 'issue',
})

Autocomplete Integration

Combine mentions with autocomplete for better UX:
import { defineMention } from '@prosekit/extensions'
import { defineAutocomplete } from '@prosekit/extensions'

const editor = createEditor({
  extensions: [
    defineMention(),
    defineAutocomplete({
      // Trigger on '@' character
      triggers: ['@'],
      
      // Fetch suggestions
      suggestions: async (query) => {
        const users = await fetchUsers(query)
        return users.map(user => ({
          label: user.name,
          value: user,
        }))
      },
      
      // Insert mention on selection
      onSelect: (item) => {
        editor.commands.insertMention({
          id: item.value.id,
          value: `@${item.value.username}`,
          kind: 'user',
        })
      },
    }),
  ],
})

HTML Rendering

Mentions render as <span> elements with data attributes:
<span data-id="user-123" data-mention="user">@john</span>

Custom Rendering

Mentions render with their display value as text content. You can style them with CSS:
.ProseMirror span[data-mention] {
  color: #0066cc;
  background-color: #e6f2ff;
  padding: 0.1em 0.3em;
  border-radius: 3px;
  cursor: pointer;
}

.ProseMirror span[data-mention="user"] {
  color: #0066cc;
}

.ProseMirror span[data-mention="tag"] {
  color: #cc6600;
}

.ProseMirror span[data-mention="channel"] {
  color: #00cc66;
}

.ProseMirror span[data-mention]:hover {
  background-color: #cce5ff;
}

Parsing Mentions

The extension automatically parses mentions from HTML:
const html = '<span data-id="user-123" data-mention="user">@john</span>'
// Automatically parsed into mention node

Extracting Mentions

Extract all mentions from the document:
function extractMentions(state) {
  const mentions = []
  
  state.doc.descendants((node) => {
    if (node.type.name === 'mention') {
      mentions.push({
        id: node.attrs.id,
        value: node.attrs.value,
        kind: node.attrs.kind,
      })
    }
  })
  
  return mentions
}

const allMentions = extractMentions(editor.state)
console.log('Mentioned users:', allMentions.filter(m => m.kind === 'user'))

Event Handling

Handle mention clicks or interactions:
editor.view.dom.addEventListener('click', (event) => {
  const target = event.target as HTMLElement
  
  if (target.hasAttribute('data-mention')) {
    const id = target.getAttribute('data-id')
    const kind = target.getAttribute('data-mention')
    const value = target.textContent
    
    console.log('Clicked mention:', { id, kind, value })
    
    // Custom handling
    if (kind === 'user') {
      openUserProfile(id)
    } else if (kind === 'tag') {
      filterByTag(id)
    }
  }
})

Complete Autocomplete Example

import { createEditor } from '@prosekit/core'
import { defineMention } from '@prosekit/extensions'

// Mock user database
const users = [
  { id: '1', username: 'john', name: 'John Doe' },
  { id: '2', username: 'jane', name: 'Jane Smith' },
  { id: '3', username: 'bob', name: 'Bob Wilson' },
]

function createMentionEditor() {
  const editor = createEditor({
    extensions: [defineMention()],
  })
  
  // Setup autocomplete UI (pseudo-code)
  let autocompleteActive = false
  let autocompleteQuery = ''
  
  editor.view.dom.addEventListener('keydown', (event) => {
    // Detect '@' trigger
    if (event.key === '@') {
      autocompleteActive = true
      autocompleteQuery = ''
    }
    
    // Update query while autocomplete is active
    if (autocompleteActive) {
      if (event.key === 'Escape') {
        autocompleteActive = false
      } else if (event.key === 'Backspace') {
        autocompleteQuery = autocompleteQuery.slice(0, -1)
      } else if (event.key.length === 1) {
        autocompleteQuery += event.key
      }
      
      // Filter and show suggestions
      const suggestions = users.filter(user => 
        user.name.toLowerCase().includes(autocompleteQuery.toLowerCase())
      )
      
      showAutocompleteSuggestions(suggestions)
    }
  })
  
  return editor
}

function selectMention(editor, user) {
  // Delete the '@query' text
  const { from, to } = editor.state.selection
  const transaction = editor.state.tr.delete(from - autocompleteQuery.length - 1, to)
  editor.view.dispatch(transaction)
  
  // Insert mention
  editor.commands.insertMention({
    id: user.id,
    value: `@${user.username}`,
    kind: 'user',
  })
}

Validation

Validate mention attributes:
function isValidMention(attrs: Partial<MentionAttrs>): boolean {
  return (
    typeof attrs.id === 'string' && attrs.id.length > 0 &&
    typeof attrs.value === 'string' && attrs.value.length > 0 &&
    typeof attrs.kind === 'string'
  )
}

// Use before inserting
const mentionAttrs = {
  id: 'user-123',
  value: '@john',
  kind: 'user',
}

if (isValidMention(mentionAttrs)) {
  editor.commands.insertMention(mentionAttrs)
}

Mention Notifications

Implement notification system:
function notifyMentionedUsers(editor) {
  const mentions = extractMentions(editor.state)
  const userMentions = mentions.filter(m => m.kind === 'user')
  
  // Send notifications
  userMentions.forEach(mention => {
    sendNotification(mention.id, {
      type: 'mention',
      message: `You were mentioned in a document`,
      documentId: currentDocumentId,
    })
  })
}

// Call when document is saved
editor.on('save', () => {
  notifyMentionedUsers(editor)
})

Styling by Kind

Style mentions differently based on their kind:
/* Base mention styles */
.ProseMirror span[data-mention] {
  padding: 0.1em 0.4em;
  border-radius: 4px;
  font-weight: 500;
  cursor: pointer;
  transition: background-color 0.2s;
}

/* User mentions */
.ProseMirror span[data-mention="user"] {
  color: #0066cc;
  background-color: #e6f2ff;
}

.ProseMirror span[data-mention="user"]:hover {
  background-color: #cce5ff;
}

/* Tag mentions */
.ProseMirror span[data-mention="tag"] {
  color: #cc6600;
  background-color: #fff2e6;
}

/* Channel mentions */
.ProseMirror span[data-mention="channel"] {
  color: #00aa55;
  background-color: #e6fff2;
}

/* Issue mentions */
.ProseMirror span[data-mention="issue"] {
  color: #8800cc;
  background-color: #f5e6ff;
  font-family: monospace;
}

Node Properties

Mentions are defined as:
  • Inline nodes that flow with text
  • Atomic nodes that cannot be split
  • Leaf text nodes that display their value attribute

Best Practices

  1. Use meaningful IDs: Ensure IDs are unique and stable
  2. Provide clear values: Display values should be human-readable
  3. Type mentions with kind: Use kind to differentiate mention types
  4. Implement autocomplete: Enhance UX with suggestion dropdowns
  5. Handle deletions: Mentions should delete as single units
  6. Style appropriately: Make mentions visually distinct from regular text
  • Use with defineAutocomplete() for suggestion dropdowns
  • Combine with defineLink() to make mentions clickable
  • Works within defineList() items and defineTable() cells