Multi Select

A rich multi-select editor.

Loading...
Files
components/select-editor-demo.tsx
'use client';

import React from 'react';
import { useForm, useWatch } from 'react-hook-form';

import { zodResolver } from '@hookform/resolvers/zod';
import { CheckIcon, PlusIcon } from 'lucide-react';
import * as z from 'zod';

import { Button } from '@/components/plate-ui/button';
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormMessage,
} from '@/components/plate-ui/form';
import {
  type SelectItem,
  SelectEditor,
  SelectEditorCombobox,
  SelectEditorContent,
  SelectEditorInput,
} from '@/components/plate-ui/select-editor';

const LABELS = [
  { url: '/docs/components/editor', value: 'Editor' },
  { url: '/docs/components/select-editor', value: 'Select Editor' },
  { url: '/docs/components/block-selection', value: 'Block Selection' },
  { url: '/docs/components/button', value: 'Button' },
  { url: '/docs/components/command', value: 'Command' },
  { url: '/docs/components/dialog', value: 'Dialog' },
  { url: '/docs/components/form', value: 'Form' },
  { url: '/docs/components/input', value: 'Input' },
  { url: '/docs/components/label', value: 'Label' },
  { url: '/docs/components/plate-element', value: 'Plate Element' },
  { url: '/docs/components/popover', value: 'Popover' },
  { url: '/docs/components/tag-element', value: 'Tag Element' },
] satisfies (SelectItem & { url: string })[];

const formSchema = z.object({
  labels: z
    .array(
      z.object({
        value: z.string(),
      })
    )
    .min(1, 'Select at least one label')
    .max(10, 'Select up to 10 labels'),
});

type FormValues = z.infer<typeof formSchema>;

export default function EditorSelectForm() {
  const [readOnly, setReadOnly] = React.useState(false);
  const form = useForm<FormValues>({
    defaultValues: {
      labels: [LABELS[0]],
    },
    resolver: zodResolver(formSchema),
  });

  const labels = useWatch({ control: form.control, name: 'labels' });

  return (
    <div className="mx-auto w-full max-w-2xl space-y-8 p-11 pl-2 pt-24">
      <Form {...form}>
        <div className="space-y-6">
          <FormField
            name="labels"
            control={form.control}
            render={({ field }) => (
              <FormItem>
                <div className="flex items-start gap-2">
                  <Button
                    variant="ghost"
                    className="h-10"
                    onClick={() => setReadOnly(!readOnly)}
                    type="button"
                  >
                    {readOnly ? (
                      <PlusIcon className="size-4" />
                    ) : (
                      <CheckIcon className="size-4" />
                    )}
                  </Button>

                  {readOnly && labels.length === 0 ? (
                    <Button
                      size="lg"
                      variant="ghost"
                      className="h-10"
                      onClick={() => {
                        setReadOnly(false);
                      }}
                      type="button"
                    >
                      Add labels
                    </Button>
                  ) : (
                    <FormControl>
                      <SelectEditor
                        value={field.value}
                        onValueChange={readOnly ? undefined : field.onChange}
                        items={LABELS}
                      >
                        <SelectEditorContent>
                          <SelectEditorInput
                            readOnly={readOnly}
                            placeholder={
                              readOnly ? 'Empty' : 'Select labels...'
                            }
                          />
                          {!readOnly && <SelectEditorCombobox />}
                        </SelectEditorContent>
                      </SelectEditor>
                    </FormControl>
                  )}
                </div>
                <FormMessage />
              </FormItem>
            )}
          />
        </div>
      </Form>
    </div>
  );
}

Features

Unlike traditional input-based multi-selects, this component is built on top of Plate editor, providing:

  • Full history support (undo/redo)
  • Native cursor navigation between and within tags
  • Select one to many tags
  • Copy/paste tags
  • Drag and drop to reorder tags
  • Read-only mode
  • Duplicate tags prevention
  • Create new tags, case insensitive
  • Search text cleanup
  • Whitespace trimming
  • Fuzzy search with cmdk

Installation

npm install @udecode/plate-tag

Usage

import { MultiSelectPlugin } from '@udecode/plate-tag/react';
import { TagElement } from '@/components/plate-ui/tag-element';
import {
  SelectEditor,
  SelectEditorContent,
  SelectEditorInput,
  SelectEditorCombobox,
  type SelectItem,
} from '@/components/plate-ui/select-editor';
 
// Define your items
const ITEMS: SelectItem[] = [
  { value: 'React' },
  { value: 'TypeScript' },
  { value: 'JavaScript' },
];
 
export default function MySelectEditor() {
  const [value, setValue] = React.useState<SelectItem[]>([ITEMS[0]]);
 
  return (
    <SelectEditor
      value={value}
      onValueChange={setValue}
      items={ITEMS}
    >
      <SelectEditorContent>
        <SelectEditorInput placeholder="Select items..." />
        <SelectEditorCombobox />
      </SelectEditorContent>
    </SelectEditor>
  );
}

Plugins

TagPlugin

Inline void element plugin.

MultiSelectPlugin

Extension of the TagPlugin that constrains the editor to tag elements.

API

editor.tf.insert.tag

Inserts a new multi-select element at the current selection.

Parameters

Collapse all

    Properties for the multi-select element:

Hooks

useSelectedItems

Hook to get the currently selected tag items in the editor.

Returns

Collapse all

    Array of selected tag items, each containing a value and any additional properties.

getSelectedItems

Gets all tag items in the editor.

Parameters

Collapse all

    The Slate editor instance.

Returns

Collapse all

    Array of tag items in the editor.

isEqualTags

Utility function to compare two sets of tags for equality, ignoring order.

Parameters

Collapse all

    The Slate editor instance.

    New set of tags to compare against the current editor tags.

Returns

Collapse all

    true if both sets contain the same values, false otherwise.

useSelectableItems

Hook to get the available items that can be selected, filtered by search and excluding already selected items.

Parameters

Collapse all

Returns

Collapse all

    Filtered array of selectable items.

useSelectEditorCombobox

Hook to handle combobox behavior in the editor, including text cleanup and item selection.

Parameters

Collapse all

Types

TTagElement

type TTagElement = TElement & {
  value: string;
  [key: string]: unknown;
};

TagLike

type TagLike = {
  value: string;
  [key: string]: unknown;
};