The fastest way to add code blocks is with CodeBlockKit. It composes CodeBlockPlugin, its required CodeLinePlugin, and the removable CodeHighlightPlugin with the shipped triple-backtick input rule and Plate UI components.
'use client';
import { BaseCodeBlockPlugin, CodeBlockRules } from '@platejs/code-block';
import {
CodeBlockPlugin,
CodeHighlightPlugin,
CodeLinePlugin,
} from '@platejs/code-block/react';
import { all, createLowlight } from 'lowlight';
import { BracesIcon, Check, CheckIcon, CopyIcon } from 'lucide-react';
import { NodeApi } from 'platejs';
import {
PlateElement,
PlateLeaf,
type PlateElementProps,
type PlateLeafProps,
useEditor,
useEditorReadOnly,
useElement,
} from 'platejs/react';
import * as React from 'react';
import { Button } from '@/components/ui/button';
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@/components/ui/command';
import { cn } from '@/lib/utils';
import {
FloatingPopover as Popover,
FloatingPopoverContent as PopoverContent,
FloatingPopoverTrigger as PopoverTrigger,
} from '@/components/editor/floating-popover';
type CodeBlockElementProps = PlateElementProps<typeof CodeBlockPlugin> & {
showLanguageLabel?: boolean;
};
const codeBlockLanguages: Array<{ label: string; value: string }> = [
{ label: 'Auto', value: 'auto' },
{ label: 'Plain Text', value: 'plaintext' },
{ label: 'ABAP', value: 'abap' },
{ label: 'Agda', value: 'agda' },
{ label: 'Arduino', value: 'arduino' },
{ label: 'ASCII Art', value: 'ascii' },
{ label: 'Assembly', value: 'x86asm' },
{ label: 'Bash', value: 'bash' },
{ label: 'BASIC', value: 'basic' },
{ label: 'BNF', value: 'bnf' },
{ label: 'C', value: 'c' },
{ label: 'C#', value: 'csharp' },
{ label: 'C++', value: 'cpp' },
{ label: 'Clojure', value: 'clojure' },
{ label: 'CoffeeScript', value: 'coffeescript' },
{ label: 'Coq', value: 'coq' },
{ label: 'CSS', value: 'css' },
{ label: 'Dart', value: 'dart' },
{ label: 'Dhall', value: 'dhall' },
{ label: 'Diff', value: 'diff' },
{ label: 'Docker', value: 'dockerfile' },
{ label: 'EBNF', value: 'ebnf' },
{ label: 'Elixir', value: 'elixir' },
{ label: 'Elm', value: 'elm' },
{ label: 'Erlang', value: 'erlang' },
{ label: 'F#', value: 'fsharp' },
{ label: 'Flow', value: 'flow' },
{ label: 'Fortran', value: 'fortran' },
{ label: 'Gherkin', value: 'gherkin' },
{ label: 'GLSL', value: 'glsl' },
{ label: 'Go', value: 'go' },
{ label: 'GraphQL', value: 'graphql' },
{ label: 'Groovy', value: 'groovy' },
{ label: 'Haskell', value: 'haskell' },
{ label: 'HCL', value: 'hcl' },
{ label: 'HTML', value: 'html' },
{ label: 'Idris', value: 'idris' },
{ label: 'Java', value: 'java' },
{ label: 'JavaScript', value: 'javascript' },
{ label: 'JSON', value: 'json' },
{ label: 'Julia', value: 'julia' },
{ label: 'Kotlin', value: 'kotlin' },
{ label: 'LaTeX', value: 'latex' },
{ label: 'Less', value: 'less' },
{ label: 'Lisp', value: 'lisp' },
{ label: 'LiveScript', value: 'livescript' },
{ label: 'LLVM IR', value: 'llvm' },
{ label: 'Lua', value: 'lua' },
{ label: 'Makefile', value: 'makefile' },
{ label: 'Markdown', value: 'markdown' },
{ label: 'Markup', value: 'markup' },
{ label: 'MATLAB', value: 'matlab' },
{ label: 'Mathematica', value: 'mathematica' },
{ label: 'Mermaid', value: 'mermaid' },
{ label: 'Nix', value: 'nix' },
{ label: 'Notion Formula', value: 'notion' },
{ label: 'Objective-C', value: 'objectivec' },
{ label: 'OCaml', value: 'ocaml' },
{ label: 'Pascal', value: 'pascal' },
{ label: 'Perl', value: 'perl' },
{ label: 'PHP', value: 'php' },
{ label: 'PowerShell', value: 'powershell' },
{ label: 'Prolog', value: 'prolog' },
{ label: 'Protocol Buffers', value: 'protobuf' },
{ label: 'PureScript', value: 'purescript' },
{ label: 'Python', value: 'python' },
{ label: 'R', value: 'r' },
{ label: 'Racket', value: 'racket' },
{ label: 'Reason', value: 'reasonml' },
{ label: 'Ruby', value: 'ruby' },
{ label: 'Rust', value: 'rust' },
{ label: 'Sass', value: 'scss' },
{ label: 'Scala', value: 'scala' },
{ label: 'Scheme', value: 'scheme' },
{ label: 'SCSS', value: 'scss' },
{ label: 'Shell', value: 'shell' },
{ label: 'Smalltalk', value: 'smalltalk' },
{ label: 'Solidity', value: 'solidity' },
{ label: 'SQL', value: 'sql' },
{ label: 'Swift', value: 'swift' },
{ label: 'TOML', value: 'toml' },
{ label: 'TypeScript', value: 'typescript' },
{ label: 'VB.Net', value: 'vbnet' },
{ label: 'Verilog', value: 'verilog' },
{ label: 'VHDL', value: 'vhdl' },
{ label: 'Visual Basic', value: 'vbnet' },
{ label: 'WebAssembly', value: 'wasm' },
{ label: 'XML', value: 'xml' },
{ label: 'YAML', value: 'yaml' },
];
function getCodeBlockLanguageLabel(lang?: string | null) {
const value = lang?.trim();
if (!value) return null;
return (
codeBlockLanguages.find((language) => language.value === value)?.label ??
value
);
}
export function CodeBlockElement({
showLanguageLabel = true,
...props
}: CodeBlockElementProps) {
const { editor, element } = props;
return (
<PlateElement
className="py-1 **:[.hljs-addition]:bg-[#f0fff4] **:[.hljs-addition]:text-[#22863a] dark:**:[.hljs-addition]:bg-[#3c5743] dark:**:[.hljs-addition]:text-[#ceead5] **:[.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable]:text-[#005cc5] dark:**:[.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable]:text-[#6596cf] **:[.hljs-built\\\\_in,.hljs-symbol]:text-[#e36209] dark:**:[.hljs-built\\\\_in,.hljs-symbol]:text-[#c3854e] **:[.hljs-bullet]:text-[#735c0f] **:[.hljs-comment,.hljs-code,.hljs-formula]:text-[#6a737d] dark:**:[.hljs-comment,.hljs-code,.hljs-formula]:text-[#6a737d] **:[.hljs-deletion]:bg-[#ffeef0] **:[.hljs-deletion]:text-[#b31d28] dark:**:[.hljs-deletion]:bg-[#473235] dark:**:[.hljs-deletion]:text-[#e7c7cb] **:[.hljs-emphasis]:italic **:[.hljs-keyword,.hljs-doctag,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language\\\\_]:text-[#d73a49] dark:**:[.hljs-keyword,.hljs-doctag,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language\\\\_]:text-[#ee6960] **:[.hljs-name,.hljs-quote,.hljs-selector-tag,.hljs-selector-pseudo]:text-[#22863a] dark:**:[.hljs-name,.hljs-quote,.hljs-selector-tag,.hljs-selector-pseudo]:text-[#36a84f] **:[.hljs-regexp,.hljs-string,.hljs-meta_.hljs-string]:text-[#032f62] dark:**:[.hljs-regexp,.hljs-string,.hljs-meta_.hljs-string]:text-[#3593ff] **:[.hljs-section]:font-bold **:[.hljs-section]:text-[#005cc5] dark:**:[.hljs-section]:text-[#61a5f2] **:[.hljs-strong]:font-bold **:[.hljs-title,.hljs-title.class\\\\_,.hljs-title.class\\\\_.inherited\\\\_\\\\_,.hljs-title.function\\\\_]:text-[#6f42c1] dark:**:[.hljs-title,.hljs-title.class\\\\_,.hljs-title.class\\\\_.inherited\\\\_\\\\_,.hljs-title.function\\\\_]:text-[#a77bfa]"
{...props}
>
<div className="relative rounded-md bg-muted/50">
<pre className="overflow-x-auto p-8 pr-4 font-mono text-sm leading-[normal] [tab-size:2] print:break-inside-avoid">
<code>{props.children}</code>
</pre>
<div
className="absolute top-1 right-1 z-10 flex gap-0.5 select-none"
contentEditable={false}
>
{element.language === 'json' && (
<Button
size="icon"
variant="ghost"
className="size-6 text-xs"
onClick={() => {
editor.plugin(BaseCodeBlockPlugin).update.format({ element });
}}
title="Format code"
>
<BracesIcon className="!size-3.5 text-muted-foreground" />
</Button>
)}
<CodeBlockCombobox showLanguageLabel={showLanguageLabel} />
<CopyButton
size="icon"
variant="ghost"
className="size-6 gap-1 text-xs text-muted-foreground"
value={() => NodeApi.string(element)}
/>
</div>
</div>
</PlateElement>
);
}
function CodeBlockCombobox({
showLanguageLabel,
}: {
showLanguageLabel: boolean;
}) {
const [open, setOpen] = React.useState(false);
const readOnly = useEditorReadOnly();
const editor = useEditor();
const element = useElement(CodeBlockPlugin);
const value = element.language || 'plaintext';
const [searchValue, setSearchValue] = React.useState('');
const items = React.useMemo(
() =>
codeBlockLanguages.filter(
(language) =>
!searchValue ||
language.label.toLowerCase().includes(searchValue.toLowerCase())
),
[searchValue]
);
if (readOnly) {
if (!showLanguageLabel) return null;
return <CodeBlockLanguageLabel lang={element.language} />;
}
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger>
<Button
size="sm"
variant="ghost"
className="h-6 justify-between gap-1 px-2 text-xs text-muted-foreground select-none"
aria-controls="code-block-language-options"
aria-expanded={open}
role="combobox"
>
{getCodeBlockLanguageLabel(value) ?? 'Plain Text'}
</Button>
</PopoverTrigger>
<PopoverContent
className="w-[200px] p-0"
id="code-block-language-options"
onFinalFocus={() => {
setSearchValue('');
}}
>
<Command shouldFilter={false}>
<CommandInput
className="h-9"
value={searchValue}
onValueChange={(innerValue) => {
setSearchValue(innerValue);
}}
placeholder="Search language..."
/>
<CommandEmpty>No language found.</CommandEmpty>
<CommandList className="h-[344px] overflow-y-auto">
<CommandGroup>
{items.map((language) => (
<CommandItem
key={language.label}
className="cursor-pointer"
value={language.value}
onSelect={(innerValue2) => {
const path = editor.read.nodes.path(element);
if (!path) return;
editor
.plugin(BaseCodeBlockPlugin)
.update.set({ language: innerValue2 }, { at: path });
setSearchValue(innerValue2);
setOpen(false);
}}
>
<Check
className={cn(
value === language.value ? 'opacity-100' : 'opacity-0'
)}
/>
{language.label}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}
function CodeBlockLanguageLabel({ lang }: { lang?: string | null }) {
const label = getCodeBlockLanguageLabel(lang);
if (!label) return null;
return (
<span className="flex h-6 items-center px-2 text-xs text-muted-foreground select-none">
{label}
</span>
);
}
function CopyButton({
value,
...props
}: { value: (() => string) | string } & Omit<
React.ComponentProps<typeof Button>,
'value'
>) {
const [hasCopied, setHasCopied] = React.useState(false);
React.useEffect(() => {
if (!hasCopied) return undefined;
const timeout = setTimeout(() => {
setHasCopied(false);
}, 2000);
return () => {
clearTimeout(timeout);
};
}, [hasCopied]);
return (
<Button
onClick={() => {
void navigator.clipboard.writeText(
typeof value === 'function' ? value() : value
);
setHasCopied(true);
}}
{...props}
>
<span className="sr-only">Copy</span>
{hasCopied ? (
<CheckIcon className="!size-3" />
) : (
<CopyIcon className="!size-3" />
)}
</Button>
);
}
export function CodeLineElement(
props: PlateElementProps<typeof CodeLinePlugin>
) {
return <PlateElement {...props} />;
}
export function CodeSyntaxLeaf(
props: PlateLeafProps<typeof CodeHighlightPlugin>
) {
return <PlateLeaf className={props.leaf.className} {...props} />;
}
const lowlight = createLowlight(all);
export const CodeBlockKit = [
CodeBlockPlugin.configure({
component: CodeBlockElement,
inputRules: [CodeBlockRules.markdown({ on: 'match' })],
shortcuts: { toggle: { keys: 'mod+alt+8' } },
}),
CodeLinePlugin.configure({ component: CodeLineElement }),
CodeHighlightPlugin.configure({
component: CodeSyntaxLeaf,
initialState: { lowlight },
}),
];'use client';
import { BaseCodeBlockPlugin, CodeBlockRules } from '@platejs/code-block';
import {
CodeBlockPlugin,
CodeHighlightPlugin,
CodeLinePlugin,
} from '@platejs/code-block/react';
import { all, createLowlight } from 'lowlight';
import { BracesIcon, Check, CheckIcon, CopyIcon } from 'lucide-react';
import { NodeApi } from 'platejs';
import {
PlateElement,
PlateLeaf,
type PlateElementProps,
type PlateLeafProps,
useEditor,
useEditorReadOnly,
useElement,
} from 'platejs/react';
CodeBlockElement: Renders code block containers.CodeLineElement: Renders individual code lines.CodeSyntaxLeaf: Renders syntax highlighted text.Add the kit to your plugins:
import { createPlateEditor } from 'platejs/react';
import { CodeBlockKit } from '@/components/editor/code-block';
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
...CodeBlockKit,
],
});import { createPlateEditor } from 'platejs/react';
import { CodeBlockKit } from '@/components/editor/code-block';
const editor = createPlateEditor
Add CodeBlockPlugin for code-block editing. It installs CodeLinePlugin as a required dependency; add CodeHighlightPlugin separately when you want Lowlight syntax highlighting.
import { CodeBlockPlugin } from '@platejs/code-block/react';
import { createPlateEditor } from 'platejs/react';
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
CodeBlockPlugin,
],
});import { CodeBlockPlugin } from '@platejs/code-block/react';
import { createPlateEditor } from 'platejs/react';
const editor = createPlateEditor({
Configure each capability where it is owned. CodeBlockPlugin owns editing, CodeLinePlugin owns line rendering, and CodeHighlightPlugin owns highlighting.
Basic Setup with All Languages:
import { CodeBlockRules } from '@platejs/code-block';
import { CodeBlockPlugin, CodeHighlightPlugin, CodeLinePlugin } from '@platejs/code-block/react';
import { all, createLowlight } from 'lowlight';
import { createPlateEditor } from 'platejs/react';
import { CodeBlockElement, CodeLineElement, CodeSyntaxLeaf } from '@/components/editor/code-block';
// Create a lowlight instance with all languages
const lowlight = createLowlight(all);
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
CodeBlockPlugin.configure({
component: CodeBlockElement,
inputRules: [CodeBlockRules.markdown({ on: 'match' })],
Custom Language Setup (Optimized Bundle):
For optimized bundle size, you can register only specific languages:
import { createLowlight } from 'lowlight';
import css from 'highlight.js/lib/languages/css';
import js from 'highlight.js/lib/languages/javascript';
import ts from 'highlight.js/lib/languages/typescript';
import html from 'highlight.js/lib/languages/xml';
// Create a lowlight instance
const lowlight = createLowlight();
// Register only the languages you need
lowlight.register('html', html);
lowlight.register('css', css);
lowlight.register('js', js);
lowlight.register('ts', ts);
CodeBlockPlugin.configure({ component }): Assigns
CodeBlockElement to render code block
containers.inputRules: Registers the triple-backtick fence rule. Use on: 'match' to commit when the fence becomes complete or on: 'break' to commit on Enter.CodeHighlightPlugin.initialState.lowlight: Lowlight instance for syntax highlighting.CodeHighlightPlugin.initialState.defaultLanguage: Default language when no language is specified.shortcuts.toggle: Defines a keyboard shortcut to toggle code blocks.component: Assigns components for code lines and syntax highlighting.For plain code blocks, omit CodeHighlightPlugin:
import { CodeBlockPlugin } from '@platejs/code-block/react';
import { createPlateEditor } from 'platejs/react';
const editor = createPlateEditor({
plugins: [CodeBlockPlugin],
});import { CodeBlockPlugin } from '@platejs/code-block/react';
import { createPlateEditor } from 'platejs/react';
const editor = createPlateEditor({
plugins: [CodeBlockPlugin],
});For the runtime model, see Plugin Input Rules.
You can add this item to the Turn Into Toolbar Button to convert blocks into code blocks:
import { PLUGINS } from 'platejs';
const turnIntoItem = {
icon: <FileCodeIcon />,
label: 'Code',
value: PLUGINS.codeBlock,
};import { PLUGINS } from 'platejs';
const turnIntoItem = {
icon: <FileCodeIcon />,
label: 'Code',
value: PLUGINS.codeBlock,
};You can add this item to the Insert Toolbar Button to insert code block elements:
import { PLUGINS } from 'platejs';
const insertItem = {
icon: <FileCodeIcon />,
label: 'Code',
value: PLUGINS.codeBlock,
};import { PLUGINS } from 'platejs';
const insertItem = {
icon: <FileCodeIcon />,
label: 'Code',
value: PLUGINS.codeBlock,
};Owns the code-block schema, editing behavior, input rules, shortcuts, and commands. CodeLinePlugin is its required dependency.
Owns the codeSyntax mark, Lowlight decoration pipeline, and highlighted token rendering. Include or omit this descriptor as one ordinary plugin-array entry.
Required structural dependency for code-block lines. Add it directly only to replace its default configuration or component.
Install CodeBlockPlugin, then mutate code blocks through its command group.
editor.update.codeBlock.insert();
editor.update.codeBlock.toggle();
editor.update.codeBlock.tab();
editor.update.codeBlock.untab();
editor.update.codeBlock.resetBlock();
editor.update.codeBlock.selectAll();editor.update.codeBlock.insert();
editor.update.codeBlock.toggle();
editor.update.codeBlock.tab();
editor.update.codeBlock.untab();
editor.update.codeBlock.resetBlock();
editor.update.codeBlock.selectAll();insert() converts an empty selected block in place. For non-empty or
expanded selections, it inserts and selects a paragraph after the range-end
block, then converts that paragraph. It is a no-op without a selection.toggle() converts selected blocks to or from code blocks.tab() and untab() indent or outdent selected code lines by two spaces.resetBlock() unwraps the selected code block into paragraphs.selectAll() selects the containing code block.import { CodeBlockRules } from '@platejs/code-block';
import { CodeBlockPlugin, CodeHighlightPlugin, CodeLinePlugin } from '@platejs/code-block/react';
import { all, createLowlight } from 'lowlight';
import { createPlateEditor } from 'platejs/react';
import { CodeBlockElement, CodeLineElement, CodeSyntaxLeaf } from '@/components/editor/code-block';
// Create a lowlight instance with all languages
const lowlight = createLowlight(all);
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
CodeBlockPlugin.configure({
component: CodeBlockElement,
inputRules: [CodeBlockRules.markdown({ on: 'match' })],
shortcuts: { toggle: { keys: 'mod+alt+8' } },
}),
CodeLinePlugin.configure({ component: CodeLineElement }),
CodeHighlightPlugin.configure({
component: CodeSyntaxLeaf,
initialState: { lowlight },
}),
],
});import { createLowlight } from 'lowlight';
import css from 'highlight.js/lib/languages/css';
import js from 'highlight.js/lib/languages/javascript';
import ts from 'highlight.js/lib/languages/typescript';
import html from 'highlight.js/lib/languages/xml';
// Create a lowlight instance
const lowlight = createLowlight();
// Register only the languages you need
lowlight.register('html', html);
lowlight.register('css', css);
lowlight.register('js', js);
lowlight.register('ts', ts);
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
CodeBlockPlugin.configure({
component: CodeBlockElement,
inputRules: [CodeBlockRules.markdown({ on: 'match' })],
shortcuts: { toggle: { keys: 'mod+alt+8' } },
}),
CodeLinePlugin.configure({ component: CodeLineElement }),
CodeHighlightPlugin.configure({
component: CodeSyntaxLeaf,
initialState: {
lowlight,
defaultLanguage: 'js', // Set default language (optional)
},
}),
],
});