Loading…
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 with case insensitive matching
- Search text cleanup and whitespace trimming
- Fuzzy search powered by cmdk
Manual Usage
Installation
pnpm add @platejs/tagpnpm add @platejs/tagAdd Plugins
import { MultiSelectPlugin } from '@platejs/tag/react';
import { createPlateEditor } from 'platejs/react';
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
MultiSelectPlugin, // Multi-select editor with tag functionality
],
});import { MultiSelectPlugin } from '@platejs/tag/react';
import { createPlateEditor } from 'platejs/react';
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
MultiSelectPlugin, // Multi-select editor with tag functionality
],
});Configure Plugins
import { MultiSelectPlugin } from '@platejs/tag/react';
import { createPlateEditor } from 'platejs/react';
import { TagElement } from '@/components/ui/tag-node';
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
MultiSelectPlugin.withComponent(TagElement),
],
});import { MultiSelectPlugin } from '@platejs/tag/react';
import { createPlateEditor } from 'platejs/react';
import { TagElement } from '@/components/ui/tag-node';
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
MultiSelectPlugin.withComponent(TagElement),
],
});MultiSelectPlugin: Extends TagPlugin and constrains the editor to only contain tag elementswithComponent: AssignsTagElementto render tag components
Add SelectEditor
npx shadcn@latest add @plate/select-editorBasic Example
import { MultiSelectPlugin } from '@platejs/tag/react';
import { TagElement } from '@/components/ui/tag-node';
import {
SelectEditor,
SelectEditorContent,
SelectEditorInput,
SelectEditorCombobox,
type SelectItem,
} from '@/components/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>
);
}import { MultiSelectPlugin } from '@platejs/tag/react';
import { TagElement } from '@/components/ui/tag-node';
import {
SelectEditor,
SelectEditorContent,
SelectEditorInput,
SelectEditorCombobox,
type SelectItem,
} from '@/components/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>
);
}Form Example
'use client';
import * as 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/ui/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from '@/components/ui/form';
import {
type SelectItem,
SelectEditor,
SelectEditorCombobox,
SelectEditorContent,
SelectEditorInput,
} from '@/components/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/popover', value: 'Popover' },
{ url: '/docs/components/tag-node', 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 pt-24 pl-2">
<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>
);
}'use client';
import * as 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/ui/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from '@/components/ui/form';
import {
type SelectItem,
SelectEditor,
SelectEditorCombobox,
SelectEditorContent,
SelectEditorInput,
} from '@/components/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/popover', value: 'Popover' },
{ url: '/docs/components/tag-node', 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 pt-24 pl-2">
<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>
);
}Plugins
TagPlugin
Inline void element plugin for individual tag functionality.
MultiSelectPlugin
Extension of TagPlugin that constrains the editor to only contain tag elements, enabling multi-select behavior with automatic text cleanup and duplicate prevention.
API
tf.insert.tag
Inserts new multi-select element at current selection.
getSelectedItems
Gets all tag items in the editor.
isEqualTags
Utility function to compare two sets of tags for equality, ignoring order.
Hooks
useSelectedItems
Hook to get the currently selected tag items in the editor.
useSelectableItems
Hook to get the available items that can be selected, filtered by search and excluding already selected items.
useSelectEditorCombobox
Hook to handle combobox behavior in the editor, including text cleanup and item selection.
Types
TTagElement
type TTagElement = TElement & {
value: string;
[key: string]: unknown;
};type TTagElement = TElement & {
value: string;
[key: string]: unknown;
};TTagProps
type TTagProps = {
value: string;
[key: string]: unknown;
};type TTagProps = {
value: string;
[key: string]: unknown;
};