Horizontal Rule adds a void hr block for separating sections. The package owns the element type, HTML parser, default <hr> render tag, and Markdown-style input rules. The registry kit adds the Plate UI divider component, static renderer, and insert toolbar item.
Fast Path
Add Basic Blocks
BasicBlocksKit installs HorizontalRulePlugin with the registry HrElement and both default input rules.
'use client';
import {
BlockquoteRules,
HeadingRules,
HorizontalRuleRules,
} from '@platejs/basic-nodes';
import {
BlockquotePlugin,
H1Plugin,
H2Plugin,
H3Plugin,
H4Plugin,
H5Plugin,
H6Plugin,
HorizontalRulePlugin,
} from '@platejs/basic-nodes/react';
import { ParagraphPlugin } from 'platejs/react';
import { BlockquoteElement } from '@/components/ui/blockquote-node';
import {
H1Element,
H2Element,
H3Element,
H4Element,
H5Element,
H6Element,
} from '@/components/ui/heading-node';
import { HrElement } from '@/components/ui/hr-node';
import { ParagraphElement } from '@/components/ui/paragraph-node';
export const BasicBlocksKit = [
ParagraphPlugin.withComponent(ParagraphElement),
H1Plugin.configure({
inputRules: [HeadingRules.markdown()],
node: {
component: H1Element,
},
rules: {
break: { empty: 'reset' },
},
shortcuts: { toggle: { keys: 'mod+alt+1' } },
}),
H2Plugin.configure({
inputRules: [HeadingRules.markdown()],
node: {
component: H2Element,
},
rules: {
break: { empty: 'reset' },
},
shortcuts: { toggle: { keys: 'mod+alt+2' } },
}),
H3Plugin.configure({
inputRules: [HeadingRules.markdown()],
node: {
component: H3Element,
},
rules: {
break: { empty: 'reset' },
},
shortcuts: { toggle: { keys: 'mod+alt+3' } },
}),
H4Plugin.configure({
inputRules: [HeadingRules.markdown()],
node: {
component: H4Element,
},
rules: {
break: { empty: 'reset' },
},
shortcuts: { toggle: { keys: 'mod+alt+4' } },
}),
H5Plugin.configure({
inputRules: [HeadingRules.markdown()],
node: {
component: H5Element,
},
rules: {
break: { empty: 'reset' },
},
shortcuts: { toggle: { keys: 'mod+alt+5' } },
}),
H6Plugin.configure({
inputRules: [HeadingRules.markdown()],
node: {
component: H6Element,
},
rules: {
break: { empty: 'reset' },
},
shortcuts: { toggle: { keys: 'mod+alt+6' } },
}),
BlockquotePlugin.configure({
inputRules: [BlockquoteRules.markdown()],
node: { component: BlockquoteElement },
shortcuts: { toggle: { keys: 'mod+shift+period' } },
}),
HorizontalRulePlugin.configure({
inputRules: [
HorizontalRuleRules.markdown({ variant: '-' }),
HorizontalRuleRules.markdown({ variant: '_' }),
],
node: {
component: HrElement,
},
}),
];'use client';
import {
BlockquoteRules,
HeadingRules,
HorizontalRuleRules,
} from '@platejs/basic-nodes';
import {
BlockquotePlugin,
H1Plugin,
H2Plugin,
H3Plugin,
H4Plugin,
H5Plugin,
H6Plugin,
HorizontalRulePlugin,
} from '@platejs/basic-nodes/react';
import { ParagraphPlugin } from 'platejs/react';
import { BlockquoteElement } from '@/components/ui/blockquote-node';
import {
H1Element,
H2Element,
H3Element,
H4Element,
H5Element,
H6Element,
} from '@/components/ui/heading-node';
import { HrElement } from '@/components/ui/hr-node';
import { ParagraphElement } from '@/components/ui/paragraph-node';
export const BasicBlocksKit = [
ParagraphPlugin.withComponent(ParagraphElement),
H1Plugin.configure({
inputRules: [HeadingRules.markdown()],
node: {
component: H1Element,
},
rules: {
break: { empty: 'reset' },
},
shortcuts: { toggle: { keys: 'mod+alt+1' } },
}),
H2Plugin.configure({
inputRules: [HeadingRules.markdown()],
node: {
component: H2Element,
},
rules: {
break: { empty: 'reset' },
},
shortcuts: { toggle: { keys: 'mod+alt+2' } },
}),
H3Plugin.configure({
inputRules: [HeadingRules.markdown()],
node: {
component: H3Element,
},
rules: {
break: { empty: 'reset' },
},
shortcuts: { toggle: { keys: 'mod+alt+3' } },
}),
H4Plugin.configure({
inputRules: [HeadingRules.markdown()],
node: {
component: H4Element,
},
rules: {
break: { empty: 'reset' },
},
shortcuts: { toggle: { keys: 'mod+alt+4' } },
}),
H5Plugin.configure({
inputRules: [HeadingRules.markdown()],
node: {
component: H5Element,
},
rules: {
break: { empty: 'reset' },
},
shortcuts: { toggle: { keys: 'mod+alt+5' } },
}),
H6Plugin.configure({
inputRules: [HeadingRules.markdown()],
node: {
component: H6Element,
},
rules: {
break: { empty: 'reset' },
},
shortcuts: { toggle: { keys: 'mod+alt+6' } },
}),
BlockquotePlugin.configure({
inputRules: [BlockquoteRules.markdown()],
node: { component: BlockquoteElement },
shortcuts: { toggle: { keys: 'mod+shift+period' } },
}),
HorizontalRulePlugin.configure({
inputRules: [
HorizontalRuleRules.markdown({ variant: '-' }),
HorizontalRuleRules.markdown({ variant: '_' }),
],
node: {
component: HrElement,
},
}),
];import { createPlateEditor } from 'platejs/react';
import { BasicBlocksKit } from '@/components/editor/plugins/basic-blocks-kit';
export const editor = createPlateEditor({
plugins: BasicBlocksKit,
});import { createPlateEditor } from 'platejs/react';
import { BasicBlocksKit } from '@/components/editor/plugins/basic-blocks-kit';
export const editor = createPlateEditor({
plugins: BasicBlocksKit,
});Render The Divider
hr-node renders the editable divider with selected/focused ring styles and a static companion component.
'use client';
import * as React from 'react';
import type { PlateElementProps } from 'platejs/react';
import {
PlateElement,
useFocused,
useReadOnly,
useSelected,
} from 'platejs/react';
import { cn } from '@/lib/utils';
export function HrElement(props: PlateElementProps) {
const readOnly = useReadOnly();
const selected = useSelected();
const focused = useFocused();
return (
<PlateElement {...props}>
<div className="py-6" contentEditable={false}>
<hr
className={cn(
'h-0.5 rounded-sm border-none bg-muted bg-clip-content',
selected && focused && 'ring-2 ring-ring ring-offset-2',
!readOnly && 'cursor-pointer'
)}
/>
</div>
{props.children}
</PlateElement>
);
}'use client';
import * as React from 'react';
import type { PlateElementProps } from 'platejs/react';
import {
PlateElement,
useFocused,
useReadOnly,
useSelected,
} from 'platejs/react';
import { cn } from '@/lib/utils';
export function HrElement(props: PlateElementProps) {
const readOnly = useReadOnly();
const selected = useSelected();
const focused = useFocused();
return (
<PlateElement {...props}>
<div className="py-6" contentEditable={false}>
<hr
className={cn(
'h-0.5 rounded-sm border-none bg-muted bg-clip-content',
selected && focused && 'ring-2 ring-ring ring-offset-2',
!readOnly && 'cursor-pointer'
)}
/>
</div>
{props.children}
</PlateElement>
);
}Add Static Rendering
Use BaseBasicBlocksKit when rendering read-only content with platejs/static.
import {
BaseBlockquotePlugin,
BaseH1Plugin,
BaseH2Plugin,
BaseH3Plugin,
BaseH4Plugin,
BaseH5Plugin,
BaseH6Plugin,
BaseHorizontalRulePlugin,
} from '@platejs/basic-nodes';
import { BaseParagraphPlugin } from 'platejs';
import { BlockquoteElementStatic } from '@/components/ui/blockquote-node-static';
import {
H1ElementStatic,
H2ElementStatic,
H3ElementStatic,
H4ElementStatic,
H5ElementStatic,
H6ElementStatic,
} from '@/components/ui/heading-node-static';
import { HrElementStatic } from '@/components/ui/hr-node-static';
import { ParagraphElementStatic } from '@/components/ui/paragraph-node-static';
export const BaseBasicBlocksKit = [
BaseParagraphPlugin.withComponent(ParagraphElementStatic),
BaseH1Plugin.withComponent(H1ElementStatic),
BaseH2Plugin.withComponent(H2ElementStatic),
BaseH3Plugin.withComponent(H3ElementStatic),
BaseH4Plugin.withComponent(H4ElementStatic),
BaseH5Plugin.withComponent(H5ElementStatic),
BaseH6Plugin.withComponent(H6ElementStatic),
BaseBlockquotePlugin.withComponent(BlockquoteElementStatic),
BaseHorizontalRulePlugin.withComponent(HrElementStatic),
];import {
BaseBlockquotePlugin,
BaseH1Plugin,
BaseH2Plugin,
BaseH3Plugin,
BaseH4Plugin,
BaseH5Plugin,
BaseH6Plugin,
BaseHorizontalRulePlugin,
} from '@platejs/basic-nodes';
import { BaseParagraphPlugin } from 'platejs';
import { BlockquoteElementStatic } from '@/components/ui/blockquote-node-static';
import {
H1ElementStatic,
H2ElementStatic,
H3ElementStatic,
H4ElementStatic,
H5ElementStatic,
H6ElementStatic,
} from '@/components/ui/heading-node-static';
import { HrElementStatic } from '@/components/ui/hr-node-static';
import { ParagraphElementStatic } from '@/components/ui/paragraph-node-static';
export const BaseBasicBlocksKit = [
BaseParagraphPlugin.withComponent(ParagraphElementStatic),
BaseH1Plugin.withComponent(H1ElementStatic),
BaseH2Plugin.withComponent(H2ElementStatic),
BaseH3Plugin.withComponent(H3ElementStatic),
BaseH4Plugin.withComponent(H4ElementStatic),
BaseH5Plugin.withComponent(H5ElementStatic),
BaseH6Plugin.withComponent(H6ElementStatic),
BaseBlockquotePlugin.withComponent(BlockquoteElementStatic),
BaseHorizontalRulePlugin.withComponent(HrElementStatic),
];Ownership
| Layer | Owner | What It Does |
|---|---|---|
@platejs/basic-nodes | Package | Exports BaseHorizontalRulePlugin and HorizontalRuleRules. |
@platejs/basic-nodes/react | Package | Exports HorizontalRulePlugin. |
basic-blocks-kit | Registry | Adds HorizontalRulePlugin with dash and underscore input rules plus HrElement. |
basic-blocks-base-kit | Registry | Adds BaseHorizontalRulePlugin.withComponent(HrElementStatic). |
hr-node | Registry UI | Renders editable and static divider components. |
| Insert toolbar | Registry UI | Inserts KEYS.hr through the generic insertBlock path. |
There is no package-specific insertHorizontalRule helper. App UI usually inserts KEYS.hr through the same block insertion helper used by other registry blocks.
Manual Setup
Install Package
pnpm add @platejs/basic-nodespnpm add @platejs/basic-nodesAdd The Plugin
Use the React plugin when the editor renders the divider UI.
import { HorizontalRuleRules } from '@platejs/basic-nodes';
import { HorizontalRulePlugin } from '@platejs/basic-nodes/react';
import { createPlateEditor } from 'platejs/react';
import { HrElement } from '@/components/ui/hr-node';
export const editor = createPlateEditor({
plugins: [
HorizontalRulePlugin.configure({
inputRules: [
HorizontalRuleRules.markdown({ variant: '-' }),
HorizontalRuleRules.markdown({ variant: '_' }),
],
node: { component: HrElement },
}),
],
});import { HorizontalRuleRules } from '@platejs/basic-nodes';
import { HorizontalRulePlugin } from '@platejs/basic-nodes/react';
import { createPlateEditor } from 'platejs/react';
import { HrElement } from '@/components/ui/hr-node';
export const editor = createPlateEditor({
plugins: [
HorizontalRulePlugin.configure({
inputRules: [
HorizontalRuleRules.markdown({ variant: '-' }),
HorizontalRuleRules.markdown({ variant: '_' }),
],
node: { component: HrElement },
}),
],
});Add Static Rendering
Use the base plugin with the static component in static rendering paths.
import { BaseHorizontalRulePlugin } from '@platejs/basic-nodes';
import { createStaticEditor } from 'platejs';
import { HrElementStatic } from '@/components/ui/hr-node-static';
export const staticEditor = createStaticEditor({
plugins: [BaseHorizontalRulePlugin.withComponent(HrElementStatic)],
});import { BaseHorizontalRulePlugin } from '@platejs/basic-nodes';
import { createStaticEditor } from 'platejs';
import { HrElementStatic } from '@/components/ui/hr-node-static';
export const staticEditor = createStaticEditor({
plugins: [BaseHorizontalRulePlugin.withComponent(HrElementStatic)],
});Value Shape
Horizontal rules are void block elements. Keep the empty text child so the node remains a valid Slate element.
const value = [
{
children: [{ text: 'Before the divider' }],
type: 'p',
},
{
children: [{ text: '' }],
type: 'hr',
},
{
children: [{ text: 'After the divider' }],
type: 'p',
},
];const value = [
{
children: [{ text: 'Before the divider' }],
type: 'p',
},
{
children: [{ text: '' }],
type: 'hr',
},
{
children: [{ text: 'After the divider' }],
type: 'p',
},
];| Field | Type | Notes |
|---|---|---|
type | 'hr' | Plugin key and node type from KEYS.hr. |
children | [{ text: '' }] | Required child for the void element. |
Input Rules
HorizontalRuleRules.markdown() converts a paragraph into an hr block and inserts an empty paragraph after it.
| Rule | Trigger | Behavior |
|---|---|---|
HorizontalRuleRules.markdown({ variant: '-' }) | type the third - after -- | Converts --- to hr, then inserts a paragraph. |
HorizontalRuleRules.markdown({ variant: '_' }) | type a space after ___ | Converts ___ to hr, then inserts a paragraph. |
The rule uses editor.tf.setNodes({ type: KEYS.hr }), so it transforms the current block instead of inserting a second divider elsewhere.
Registry UI
| Surface | Behavior |
|---|---|
| Editable element | Renders a non-editable padded wrapper with an <hr> inside it. |
| Selected + focused | Adds a focus ring around the divider. |
| Read-only | Removes the pointer cursor from the editable component. |
| Static element | Renders the same divider inside SlateElement. |
| Insert toolbar | Adds a Divider item with MinusIcon and KEYS.hr. |
The divider component renders {props.children} after the non-editable wrapper so Slate can still keep the void child mounted.
API Reference
| API | Package | Use |
|---|---|---|
BaseHorizontalRulePlugin | @platejs/basic-nodes | Headless void hr block with HTML deserialization and default render tag. |
HorizontalRulePlugin | @platejs/basic-nodes/react | React horizontal rule plugin. |
HorizontalRuleRules.markdown({ variant: '-' }) | @platejs/basic-nodes | Creates the dash input rule for ---. |
HorizontalRuleRules.markdown({ variant: '_' }) | @platejs/basic-nodes | Creates the underscore input rule for ___ . |