Features

  • Utilities for creating trigger-based combobox functionality
  • Configurable trigger characters and patterns
  • Keyboard navigation and selection handling

Create a Combobox Plugin

Installation

pnpm add @platejs/combobox

Create Input Plugin

First, create an input plugin that will be inserted when the trigger is activated:

import { createSlatePlugin } from 'platejs';
 
const TagInputPlugin = createSlatePlugin({
  key: 'tag_input',
  editOnly: true,
  node: {
    isElement: true,
    isInline: true,
    isVoid: true,
  },
});

Create Main Plugin

Create your main plugin using withTriggerCombobox:

import { createTSlatePlugin, type PluginConfig } from 'platejs';
import { 
  type TriggerComboboxPluginOptions, 
  withTriggerCombobox 
} from '@platejs/combobox';
 
type TagConfig = PluginConfig<'tag', TriggerComboboxPluginOptions>;
 
export const TagPlugin = createTSlatePlugin<TagConfig>({
  key: 'tag',
  node: { isElement: true, isInline: true, isVoid: true },
  options: {
    trigger: '#',
    triggerPreviousCharPattern: /^\s?$/,
    createComboboxInput: () => ({
      children: [{ text: '' }],
      type: 'tag_input',
    }),
  },
  plugins: [TagInputPlugin],
}).overrideEditor(withTriggerCombobox);
  • node.isElement: Defines this as an element node (not text)
  • node.isInline: Makes the tag element inline (not block)
  • node.isVoid: Prevents editing inside the tag element
  • options.trigger: Character that triggers the combobox (in this case #)
  • options.triggerPreviousCharPattern: RegExp pattern that must match the character before the trigger. /^\s?$/ allows the trigger at the start of a line or after whitespace
  • options.createComboboxInput: Function that creates the input element node when the trigger is activated

Create Component

Create the input element component using InlineCombobox:

import { PlateElement, useFocused, useReadOnly, useSelected } from 'platejs/react';
import {
  InlineCombobox,
  InlineComboboxContent,
  InlineComboboxEmpty,
  InlineComboboxInput,
  InlineComboboxItem,
} from '@/components/ui/inline-combobox';
import { cn } from '@/lib/utils';
 
const tags = [
  { id: 'frontend', name: 'Frontend', color: 'blue' },
  { id: 'backend', name: 'Backend', color: 'green' },
  { id: 'design', name: 'Design', color: 'purple' },
  { id: 'urgent', name: 'Urgent', color: 'red' },
];
 
export function TagInputElement({ element, ...props }) {
  return (
    <PlateElement as="span" {...props}>
      <InlineCombobox element={element} trigger="#">
        <InlineComboboxInput />
        
        <InlineComboboxContent>
          <InlineComboboxEmpty>No tags found</InlineComboboxEmpty>
          
          {tags.map((tag) => (
            <InlineComboboxItem
              key={tag.id}
              value={tag.name}
              onClick={() => {
                // Insert actual tag element
                editor.tf.insertNodes({
                  type: 'tag',
                  tagId: tag.id,
                  children: [{ text: tag.name }],
                });
              }}
            >
              <span 
                className={`w-3 h-3 rounded-full bg-${tag.color}-500 mr-2`}
              />
              #{tag.name}
            </InlineComboboxItem>
          ))}
        </InlineComboboxContent>
      </InlineCombobox>
      
      {props.children}
    </PlateElement>
  );
}
 
export function TagElement({ element, ...props }) {
  const selected = useSelected();
  const focused = useFocused();
  const readOnly = useReadOnly();
 
  return (
    <PlateElement
      {...props}
      className={cn(
        'inline-block rounded-md bg-primary/10 px-1.5 py-0.5 align-baseline text-sm font-medium text-primary',
        !readOnly && 'cursor-pointer',
        selected && focused && 'ring-2 ring-ring'
      )}
      attributes={{
        ...props.attributes,
        contentEditable: false,
        'data-slate-value': element.value,
      }}
    >
      #{element.value}
      {props.children}
    </PlateElement>
  );
}

Add to Editor

import { createPlateEditor } from 'platejs/react';
import { TagPlugin, TagInputPlugin } from './tag-plugin';
import { TagElement, TagInputElement } from './tag-components';
 
const editor = createPlateEditor({
  plugins: [
    // ...otherPlugins,
    TagPlugin.configure({
      options: {
        triggerQuery: (editor) => {
          // Disable in code blocks
          return !editor.api.some({ match: { type: 'code_block' } });
        },
      },
    }).withComponent(TagElement),
    TagInputPlugin.withComponent(TagInputElement),
  ],
});
  • options.triggerQuery: Optional function to conditionally enable/disable the trigger based on editor state

Examples

Loading...
Files
components/demo.tsx
'use client';

import * as React from 'react';

import { Plate, usePlateEditor } from 'platejs/react';

import { EditorKit } from '@/components/editor/editor-kit';
import { Editor, EditorContainer } from '@/components/ui/editor';

import { DEMO_VALUES } from './values/demo-values';

export default function Demo({ id }: { id: string }) {
  const editor = usePlateEditor({
    plugins: EditorKit,
    value: DEMO_VALUES[id],
  });

  return (
    <Plate editor={editor}>
      <EditorContainer variant="demo">
        <Editor />
      </EditorContainer>
    </Plate>
  );
}
Loading...
Files
components/demo.tsx
'use client';

import * as React from 'react';

import { Plate, usePlateEditor } from 'platejs/react';

import { EditorKit } from '@/components/editor/editor-kit';
import { Editor, EditorContainer } from '@/components/ui/editor';

import { DEMO_VALUES } from './values/demo-values';

export default function Demo({ id }: { id: string }) {
  const editor = usePlateEditor({
    plugins: EditorKit,
    value: DEMO_VALUES[id],
  });

  return (
    <Plate editor={editor}>
      <EditorContainer variant="demo">
        <Editor />
      </EditorContainer>
    </Plate>
  );
}
Loading...
Files
components/demo.tsx
'use client';

import * as React from 'react';

import { Plate, usePlateEditor } from 'platejs/react';

import { EditorKit } from '@/components/editor/editor-kit';
import { Editor, EditorContainer } from '@/components/ui/editor';

import { DEMO_VALUES } from './values/demo-values';

export default function Demo({ id }: { id: string }) {
  const editor = usePlateEditor({
    plugins: EditorKit,
    value: DEMO_VALUES[id],
  });

  return (
    <Plate editor={editor}>
      <EditorContainer variant="demo">
        <Editor />
      </EditorContainer>
    </Plate>
  );
}

Options

TriggerComboboxPluginOptions

Configuration options for trigger-based combobox plugins.

Options

Collapse all

    Function to create the input node when trigger is activated.

    Character(s) that trigger the combobox. Can be:

    • A single character (e.g. '@')
    • An array of characters
    • A regular expression

    Pattern to match the character before trigger.

    • Example: /^\s?$/ matches start of line or space

    Custom query function to control when trigger is active.

Hooks

useComboboxInput

Hook for managing combobox input behavior and keyboard interactions.

Options

Collapse all

    Reference to the input element.

    Auto focus the input when mounted.

    • Default: true

    Cancel on arrow keys.

    • Default: true

    Cancel on backspace at start.

    • Default: true

    Cancel on blur.

    • Default: true

    Cancel when deselected.

    • Default: true

    Cancel on escape key.

    • Default: true

    Current cursor position state.

    Forward undo/redo to editor.

    • Default: true

    Callback when input is cancelled.

Returns

Collapse all

    Function to cancel the input.

    Props for the input element.

    Function to remove the input node.

useHTMLInputCursorState

Hook for tracking cursor position in an HTML input element.

Parameters

Collapse all

    Reference to the input element to track.

Returns

Collapse all

    Whether cursor is at the start of input.

    Whether cursor is at the end of input.