Horizontal Rule

PreviousNext

Void divider blocks rendered as horizontal rules.

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.

Loading…

Features

  • Void block hr element.
  • HTML deserialization from <hr>.
  • Default render tag of <hr>.
  • --- and ___ input rules through HorizontalRuleRules.markdown().
  • Editable and static registry divider components.
  • Insert toolbar item labeled Divider.

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

LayerOwnerWhat It Does
@platejs/basic-nodesPackageExports BaseHorizontalRulePlugin and HorizontalRuleRules.
@platejs/basic-nodes/reactPackageExports HorizontalRulePlugin.
basic-blocks-kitRegistryAdds HorizontalRulePlugin with dash and underscore input rules plus HrElement.
basic-blocks-base-kitRegistryAdds BaseHorizontalRulePlugin.withComponent(HrElementStatic).
hr-nodeRegistry UIRenders editable and static divider components.
Insert toolbarRegistry UIInserts 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-nodes
pnpm add @platejs/basic-nodes

Add 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',
  },
];
FieldTypeNotes
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.

RuleTriggerBehavior
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

SurfaceBehavior
Editable elementRenders a non-editable padded wrapper with an <hr> inside it.
Selected + focusedAdds a focus ring around the divider.
Read-onlyRemoves the pointer cursor from the editable component.
Static elementRenders the same divider inside SlateElement.
Insert toolbarAdds 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

APIPackageUse
BaseHorizontalRulePlugin@platejs/basic-nodesHeadless void hr block with HTML deserialization and default render tag.
HorizontalRulePlugin@platejs/basic-nodes/reactReact horizontal rule plugin.
HorizontalRuleRules.markdown({ variant: '-' })@platejs/basic-nodesCreates the dash input rule for ---.
HorizontalRuleRules.markdown({ variant: '_' })@platejs/basic-nodesCreates the underscore input rule for ___ .