Plate offers two approaches for implementing lists:
This List Classic plugin - Traditional HTML-spec lists with strict nesting rules:
ul/ol > li)The List plugin - Flexible indentation-based lists:
Choose based on your needs:
HTML-compliant lists:
ul/ol > li structureList types:
Drag & drop:
Shortcuts:
-, *, 1., [ ]) to create listsLimitations (use the List plugin for these features):
For a more flexible, Word-like approach, see the List plugin.
The fastest way to add list functionality is with the ListKit export from list-classic, which includes pre-configured list plugins, the classic markdown entry rules, Plate UI components, and keyboard shortcuts.
'use client';
import {
type BaseListPlugin,
BulletedListRules,
OrderedListRules,
TaskListRules,
} from '@platejs/list-classic';
import {
BulletedListPlugin,
ListItemContentPlugin,
ListItemPlugin,
ListPlugin,
NumberedListPlugin,
TaskListPlugin,
} from '@platejs/list-classic/react';
import { type VariantProps, cva } from 'class-variance-authority';
import type { ElementWith } from 'platejs';
import {
type PlateElementProps,
PlateElement,
useEditor,
useEditorReadOnly,
} from 'platejs/react';
import * as React from 'react';
import { Checkbox } from '@/components/ui/checkbox';
import { cn } from '@/lib/utils';
const listVariants = cva('m-0 py-1 ps-6', {
variants: {
variant: {
ol: 'list-decimal',
ul: 'list-disc [&_ul]:list-[circle] [&_ul_ul]:list-[square]',
},
},
});
type ListItemElementProps = Omit<
PlateElementProps<typeof ListItemPlugin>,
'element'
> & {
element: PlateElementProps<typeof ListItemPlugin>['element'] &
ElementWith<typeof BaseListPlugin>;
};
export function ListElement({
variant,
...props
}: PlateElementProps<typeof BulletedListPlugin> &
VariantProps<typeof listVariants> & {
variant: NonNullable<VariantProps<typeof listVariants>['variant']>;
}) {
return (
<PlateElement as={variant} className={listVariants({ variant })} {...props}>
{props.children}
</PlateElement>
);
}
export function BulletedListElement(
props: PlateElementProps<typeof BulletedListPlugin>
) {
return <ListElement variant="ul" {...props} />;
}
export function NumberedListElement(
props: PlateElementProps<typeof NumberedListPlugin>
) {
return (
<PlateElement
as="ol"
className={listVariants({ variant: 'ol' })}
{...props}
>
{props.children}
</PlateElement>
);
}
export function TaskListElement(
props: PlateElementProps<typeof TaskListPlugin>
) {
return (
<PlateElement as="ul" className="m-0 list-none! py-1 ps-6" {...props}>
{props.children}
</PlateElement>
);
}
export function ListItemElement(props: ListItemElementProps) {
const isTaskList = 'checked' in props.element;
if (isTaskList) {
return <TaskListItemElement {...props} />;
}
return <BaseListItemElement {...props} />;
}
export function BaseListItemElement(props: ListItemElementProps) {
return (
<PlateElement as="li" {...props}>
{props.children}
</PlateElement>
);
}
export function TaskListItemElement(props: ListItemElementProps) {
const { element } = props;
const editor = useEditor();
const readOnly = useEditorReadOnly();
const checked = !!element.checked;
const [firstChild, ...otherChildren] = React.Children.toArray(props.children);
return (
<BaseListItemElement {...props}>
<div
className={cn(
'flex items-stretch *:nth-[2]:flex-1 *:nth-[2]:focus:outline-none',
{
'*:nth-[2]:text-muted-foreground *:nth-[2]:line-through': checked,
}
)}
>
<div
className="-ms-5 me-1.5 flex w-fit items-start justify-center pt-[0.275em] select-none"
contentEditable={false}
>
<Checkbox
checked={checked}
disabled={readOnly}
onCheckedChange={(value) => {
if (readOnly) return;
editor.update.nodes.set({ checked: !!value }, { at: element });
}}
/>
</div>
{firstChild}
</div>
{otherChildren}
</BaseListItemElement>
);
}
export const ListKit = [
ListPlugin.configure({
inputRules: [
BulletedListRules.markdown({ variant: '-' }),
BulletedListRules.markdown({ variant: '*' }),
OrderedListRules.markdown({ variant: '.' }),
OrderedListRules.markdown({ variant: ')' }),
TaskListRules.markdown({ checked: false }),
TaskListRules.markdown({ checked: true }),
],
shortcuts: {
toggleBulleted: {
handler: ({ editor }) => {
editor.plugin(ListPlugin).update.toggle({
type: editor.plugin(BulletedListPlugin).schema.type,
});
},
keys: 'mod+alt+5',
},
toggleNumbered: {
handler: ({ editor }) => {
editor.plugin(ListPlugin).update.toggle({
type: editor.plugin(NumberedListPlugin).schema.type,
});
},
keys: 'mod+alt+6',
},
toggleTask: {
handler: ({ editor }) => {
editor.plugin(ListPlugin).update.toggle({
type: editor.plugin(TaskListPlugin).schema.type,
});
},
keys: 'mod+alt+7',
},
},
}),
ListItemContentPlugin,
BulletedListPlugin.configure({ component: BulletedListElement }),
NumberedListPlugin.configure({ component: NumberedListElement }),
TaskListPlugin.configure({ component: TaskListElement }),
ListItemPlugin.configure({ component: ListItemElement }),
];'use client';
import {
type BaseListPlugin,
BulletedListRules,
OrderedListRules,
TaskListRules,
} from '@platejs/list-classic';
import {
BulletedListPlugin,
ListItemContentPlugin,
ListItemPlugin,
ListPlugin,
NumberedListPlugin,
TaskListPlugin,
} from '@platejs/list-classic/react';
import { type VariantProps, cva } from 'class-variance-authority';
import type { ElementWith } from 'platejs';
import {
type PlateElementProps,
PlateElement,
useEditor,
BulletedListElement: Renders unordered list elements.NumberedListElement: Renders ordered list elements.TaskListElement: Renders task list elements with checkboxes.Add the kit to your plugins:
import { createPlateEditor } from 'platejs/react';
import { ListKit } from '@/components/editor/list-classic';
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
...ListKit,
],
});import { createPlateEditor } from 'platejs/react';
import { ListKit } from '@/components/editor/list-classic';
const editor = createPlateEditor
The shipped ListKit also wires the classic markdown entry rules for bulleted, ordered, and task lists. See Plugin Input Rules for the runtime model.
Add ListPlugin to your Plate plugin array. It installs the required bulleted,
numbered, task-list, list-item, and list-item-content descriptors.
import { ListPlugin } from '@platejs/list-classic/react';
import { createPlateEditor } from 'platejs/react';
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
ListPlugin,
],
});import { ListPlugin } from '@platejs/list-classic/react';
import { createPlateEditor } from 'platejs/react';
const editor = createPlateEditor({
Configure the plugins with custom components and keyboard shortcuts.
import {
BulletedListRules,
OrderedListRules,
TaskListRules,
} from '@platejs/list-classic';
import {
BulletedListPlugin,
ListPlugin,
NumberedListPlugin,
TaskListPlugin,
} from '@platejs/list-classic/react';
import { createPlateEditor } from 'platejs/react';
import { BulletedListElement, NumberedListElement, TaskListElement } from '@/components/editor/list-classic';
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
ListPlugin.configure({
inputRules: [
BulletedListRules.markdown({ variant:
inputRules: Registers the classic markdown entry rules for bulleted, ordered, and task lists..configure({ component }): Assigns BulletedListElement, NumberedListElement, and TaskListElement to render list elements.shortcuts.toggle: Defines keyboard shortcuts to toggle list types (mod+alt+5 for bulleted, mod+alt+6 for numbered, mod+alt+7 for task lists).You can add ListToolbarButton to your Toolbar to create and manage lists.
When using the ListPlugin, use the turn-into-toolbar-classic-button which includes all list types (bulleted, numbered, and task lists).
When using the ListPlugin, use the insert-toolbar-classic-button which includes all list types (bulleted, numbered, and task lists).
Installs these required element plugins:
BulletedListPluginNumberedListPluginTaskListPluginListItemPluginListItemContentPluginWhether Shift+Tab should reset list indent level.
Whether to inherit the checked state of above node after insert break at the end. Only applies to task lists.
falseWhether to inherit the checked state of below node after insert break at the start. Only applies to task lists.
falsePlugin for unordered (bulleted) lists.
Plugin for ordered (numbered) lists.
Plugin for task lists with checkboxes.
Plugin for list items. Configure validLiChildren here when another element
plugin may stay as a direct list-item child:
ListItemPlugin.configure({ initialState: { validLiChildren: [ImagePlugin] } })
Add the configured ListItemPlugin beside ListPlugin in the editor plugin
array. The explicit descriptor replaces the required default with the same name.
Plugin for list item content.
Toggles a bulleted list (ul).
Example Shortcut: Mod+Alt+5
Toggles an ordered list (ol).
Example Shortcut: Mod+Alt+6
Toggles a task list with checkboxes.
Example Shortcut: Mod+Alt+7
Finds the highest end list that can be deleted. The path of the list should be different from diffListPath. If the highest end list has 2 or more items, returns liPath. It traverses up the parent lists until:
diffListPathReturns the nearest li and ul/ol wrapping node entries for a given path (default = selection).
Searches upward for root list element.
Gets array of supported list types.
Moves list siblings after cursor to specified path.
Removes first list item if not nested and not first child.
Removes list item and moves sublist to parent if any.
Checks if selection is inside list of specific type.
Decreases indentation level of list items.
Removes list formatting from selected items.
The copied classic-list node owns todo checkbox state and updates. The copied toolbar button owns pressed state and list toggling.
'use client';
import {
type BaseListPlugin,
BulletedListRules,
OrderedListRules,
TaskListRules,
} from '@platejs/list-classic';
import {
BulletedListPlugin,
ListItemContentPlugin,
ListItemPlugin,
ListPlugin,
NumberedListPlugin,
TaskListPlugin,
} from '@platejs/list-classic/react';
import { type VariantProps, cva } from 'class-variance-authority';
import type { ElementWith } from 'platejs';
import {
type PlateElementProps,
PlateElement,
useEditor,
useEditorReadOnly,
} from 'platejs/react';
import * as React from 'react';
import { Checkbox } from '@/components/ui/checkbox';
import { cn } from '@/lib/utils';
const listVariants = cva('m-0 py-1 ps-6', {
variants: {
variant: {
ol: 'list-decimal',
ul: 'list-disc [&_ul]:list-[circle] [&_ul_ul]:list-[square]',
},
},
});
type ListItemElementProps = Omit<
PlateElementProps<typeof ListItemPlugin>,
'element'
> & {
element: PlateElementProps<typeof ListItemPlugin>['element'] &
ElementWith<typeof BaseListPlugin>;
};
export function ListElement({
variant,
...props
}: PlateElementProps<typeof BulletedListPlugin> &
VariantProps<typeof listVariants> & {
variant: NonNullable<VariantProps<typeof listVariants>['variant']>;
}) {
return (
<PlateElement as={variant} className={listVariants({ variant })} {...props}>
{props.children}
</PlateElement>
);
}
export function BulletedListElement(
props: PlateElementProps<typeof BulletedListPlugin>
) {
return <ListElement variant="ul" {...props} />;
}
export function NumberedListElement(
props: PlateElementProps<typeof NumberedListPlugin>
) {
return (
<PlateElement
as="ol"
className={listVariants({ variant: 'ol' })}
{...props}
>
{props.children}
</PlateElement>
);
}
export function TaskListElement(
props: PlateElementProps<typeof TaskListPlugin>
) {
return (
<PlateElement as="ul" className="m-0 list-none! py-1 ps-6" {...props}>
{props.children}
</PlateElement>
);
}
export function ListItemElement(props: ListItemElementProps) {
const isTaskList = 'checked' in props.element;
if (isTaskList) {
return <TaskListItemElement {...props} />;
}
return <BaseListItemElement {...props} />;
}
export function BaseListItemElement(props: ListItemElementProps) {
return (
<PlateElement as="li" {...props}>
{props.children}
</PlateElement>
);
}
export function TaskListItemElement(props: ListItemElementProps) {
const { element } = props;
const editor = useEditor();
const readOnly = useEditorReadOnly();
const checked = !!element.checked;
const [firstChild, ...otherChildren] = React.Children.toArray(props.children);
return (
<BaseListItemElement {...props}>
<div
className={cn(
'flex items-stretch *:nth-[2]:flex-1 *:nth-[2]:focus:outline-none',
{
'*:nth-[2]:text-muted-foreground *:nth-[2]:line-through': checked,
}
)}
>
<div
className="-ms-5 me-1.5 flex w-fit items-start justify-center pt-[0.275em] select-none"
contentEditable={false}
>
<Checkbox
checked={checked}
disabled={readOnly}
onCheckedChange={(value) => {
if (readOnly) return;
editor.update.nodes.set({ checked: !!value }, { at: element });
}}
/>
</div>
{firstChild}
</div>
{otherChildren}
</BaseListItemElement>
);
}
export const ListKit = [
ListPlugin.configure({
inputRules: [
BulletedListRules.markdown({ variant: '-' }),
BulletedListRules.markdown({ variant: '*' }),
OrderedListRules.markdown({ variant: '.' }),
OrderedListRules.markdown({ variant: ')' }),
TaskListRules.markdown({ checked: false }),
TaskListRules.markdown({ checked: true }),
],
shortcuts: {
toggleBulleted: {
handler: ({ editor }) => {
editor.plugin(ListPlugin).update.toggle({
type: editor.plugin(BulletedListPlugin).schema.type,
});
},
keys: 'mod+alt+5',
},
toggleNumbered: {
handler: ({ editor }) => {
editor.plugin(ListPlugin).update.toggle({
type: editor.plugin(NumberedListPlugin).schema.type,
});
},
keys: 'mod+alt+6',
},
toggleTask: {
handler: ({ editor }) => {
editor.plugin(ListPlugin).update.toggle({
type: editor.plugin(TaskListPlugin).schema.type,
});
},
keys: 'mod+alt+7',
},
},
}),
ListItemContentPlugin,
BulletedListPlugin.configure({ component: BulletedListElement }),
NumberedListPlugin.configure({ component: NumberedListElement }),
TaskListPlugin.configure({ component: TaskListElement }),
ListItemPlugin.configure({ component: ListItemElement }),
];'use client';
import {
type BaseListPlugin,
BulletedListRules,
OrderedListRules,
TaskListRules,
} from '@platejs/list-classic';
import {
BulletedListPlugin,
ListItemContentPlugin,
ListItemPlugin,
ListPlugin,
NumberedListPlugin,
TaskListPlugin,
} from '@platejs/list-classic/react';
import { type VariantProps, cva } from 'class-variance-authority';
import type { ElementWith } from 'platejs';
import {
type PlateElementProps,
PlateElement,
useEditor,
'use client';
import { ListPlugin } from '@platejs/list-classic/react';
import {
IndentIcon,
List,
ListOrdered,
ListTodo,
OutdentIcon,
} from 'lucide-react';
import { PLUGINS, type PluginReference } from 'platejs';
import { useEditor, useEditorSelector } from 'platejs/react';
import * as React from 'react';
import { ToolbarButton } from '@/components/editor/toolbar';
const pluginMap: Record<string, { icon: React.JSX.Element; label: string }> = {
[PLUGINS.numberedList]: {
icon: <ListOrdered />,
label: 'Numbered List',
},
[PLUGINS.taskList]: { icon: <ListTodo />, label: 'Task List' },
[PLUGINS.bulletedList]: { icon: <List />, label: 'Bulleted List' },
};
export function ListToolbarButton({
plugin = PLUGINS.bulletedList,
...props
}: React.ComponentProps<typeof ToolbarButton> & {
plugin?: PluginReference | string;
}) {
const editor = useEditor();
const pressed = useEditorSelector(
(innerEditor) =>
!!innerEditor.read.selection() &&
innerEditor.read.nodes.some({
type:
typeof plugin === 'string'
? innerEditor.plugin(plugin).schema.type
: plugin,
})
);
const name = typeof plugin === 'string' ? plugin : plugin.name;
const { icon, label } = pluginMap[name] ?? pluginMap[PLUGINS.bulletedList];
return (
<ToolbarButton
{...props}
pressed={pressed}
onClick={() => {
editor.plugin(ListPlugin).update.toggle({
type: editor.plugin(plugin).schema.type,
});
editor.api.dom.focus();
}}
onMouseDown={(event) => {
event.preventDefault();
}}
tooltip={label}
>
{icon}
</ToolbarButton>
);
}
export function IndentToolbarButton({
reverse = false,
...props
}: React.ComponentProps<typeof ToolbarButton> & {
reverse?: boolean;
}) {
const editor = useEditor();
return (
<ToolbarButton
{...props}
onClick={() => {
const list = editor.plugin(ListPlugin);
if (reverse) {
list.update.outdent();
} else {
list.update.indent();
}
}}
tooltip={reverse ? 'Outdent' : 'Indent'}
>
{reverse ? <OutdentIcon /> : <IndentIcon />}
</ToolbarButton>
);
}'use client';
import { ListPlugin } from '@platejs/list-classic/react';
import {
IndentIcon,
List,
ListOrdered,
ListTodo,
OutdentIcon,
} from 'lucide-react';
import { PLUGINS, type PluginReference } from 'platejs';
import { useEditor, useEditorSelector } from 'platejs/react';
import * as React from 'react';
import { ToolbarButton } from '@/components/editor/toolbar';
const pluginMap: Record<string, { icon: React
import {
BulletedListRules,
OrderedListRules,
TaskListRules,
} from '@platejs/list-classic';
import {
BulletedListPlugin,
ListPlugin,
NumberedListPlugin,
TaskListPlugin,
} from '@platejs/list-classic/react';
import { createPlateEditor } from 'platejs/react';
import { BulletedListElement, NumberedListElement, TaskListElement } from '@/components/editor/list-classic';
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
ListPlugin.configure({
inputRules: [
BulletedListRules.markdown({ variant: '-' }),
BulletedListRules.markdown({ variant: '*' }),
OrderedListRules.markdown({ variant: '.' }),
OrderedListRules.markdown({ variant: ')' }),
TaskListRules.markdown({ checked: false }),
TaskListRules.markdown({ checked: true }),
],
}),
BulletedListPlugin.configure({
component: BulletedListElement,
shortcuts: { toggle: { keys: 'mod+alt+5' } },
}),
NumberedListPlugin.configure({
component: NumberedListElement,
shortcuts: { toggle: { keys: 'mod+alt+6' } },
}),
TaskListPlugin.configure({
component: TaskListElement,
shortcuts: { toggle: { keys: 'mod+alt+7' } },
}),
],
});