The fastest way to add code blocks is with CodeBlockKit. It composes CodeBlockPlugin and the removable CodeHighlightPlugin with the shipped triple-backtick input rule and Plate UI component.
'use client';
import { all, createLowlight } from 'lowlight';
import { BracesIcon, Check, CheckIcon, CopyIcon } from 'lucide-react';
import { BaseCodeBlockPlugin, CodeBlockRules } from 'platejs';
import {
CodeBlockPlugin,
CodeHighlightPlugin,
EditorElement,
type EditorElementProps,
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,
FloatingPopoverContent,
FloatingPopoverTrigger,
} from '@/components/editor/floating-popover';
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
}: EditorElementProps<typeof CodeBlockPlugin> & {
showLanguageLabel?: boolean;
}) {
return (
<CodeBlockContainer
elementProps={props}
showLanguageLabel={showLanguageLabel}
>
<pre className="overflow-x-auto p-8 pr-4 font-mono text-sm leading-[normal] [tab-size:2] print:break-inside-avoid">
<code className="[&>[data-editor-node=text]]:contents">
{props.children}
</code>
</pre>
</CodeBlockContainer>
);
}
export function CodeBlockContainer({
children,
elementProps,
languageOptions,
showLanguageLabel = true,
}: {
children: React.ReactNode;
elementProps: EditorElementProps<typeof CodeBlockPlugin>;
languageOptions?: Array<{ label: string; value: string }>;
showLanguageLabel?: boolean;
}) {
const { editor, element, attributes, plugin, ref, slots } = elementProps;
return (
<EditorElement
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-temeditor-tag,.hljs-temeditor-variable,.hljs-type,.hljs-variable.language\\\\_]:text-[#d73a49] dark:**:[.hljs-keyword,.hljs-doctag,.hljs-temeditor-tag,.hljs-temeditor-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]"
attributes={attributes}
element={element}
plugin={plugin}
ref={ref}
slots={slots}
>
<div className="relative rounded-md bg-muted/50">
{children}
<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
languageOptions={languageOptions}
showLanguageLabel={showLanguageLabel}
/>
<CodeBlockCopyButton />
</div>
</div>
</EditorElement>
);
}
function CodeBlockCopyButton() {
const editor = useEditor();
const element = useElement(CodeBlockPlugin);
return (
<CopyButton
size="icon"
variant="ghost"
className="size-6 gap-1 text-xs text-muted-foreground"
value={() => {
const path = editor.read.nodes.path(element);
return path ? editor.read.text.string(path) : '';
}}
/>
);
}
function CodeBlockCombobox({
languageOptions = codeBlockLanguages,
showLanguageLabel,
}: {
showLanguageLabel: boolean;
languageOptions?: Array<{ label: string; value: string }>;
}) {
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(
() =>
languageOptions.filter(
(language) =>
!searchValue ||
language.label.toLowerCase().includes(searchValue.toLowerCase())
),
[searchValue, languageOptions]
);
if (readOnly) {
if (!showLanguageLabel) return null;
return <CodeBlockLanguageLabel lang={element.language} />;
}
return (
<FloatingPopover open={open} onOpenChange={setOpen}>
<FloatingPopoverTrigger>
<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>
</FloatingPopoverTrigger>
<FloatingPopoverContent
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>
</FloatingPopoverContent>
</FloatingPopover>
);
}
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 [status, setStatus] = React.useState<
'idle' | 'copying' | 'copied' | 'failed'
>('idle');
React.useEffect(() => {
if (status !== 'copied') return undefined;
const timeout = setTimeout(() => {
setStatus('idle');
}, 2000);
return () => {
clearTimeout(timeout);
};
}, [status]);
return (
<Button
disabled={status === 'copying'}
onClick={async () => {
setStatus('copying');
try {
await navigator.clipboard.writeText(
typeof value === 'function' ? value() : value
);
setStatus('copied');
} catch {
setStatus('failed');
}
}}
title={status === 'failed' ? 'Copy failed. Try again.' : 'Copy'}
{...props}
>
<span className="sr-only">
{status === 'failed' ? 'Copy failed. Try again.' : 'Copy'}
</span>
{status === 'copied' ? (
<CheckIcon className="!size-3" />
) : (
<CopyIcon className="!size-3" />
)}
</Button>
);
}
const lowlight = createLowlight(all);
export const createCodeBlockPlugin = (component: typeof CodeBlockElement) =>
CodeBlockPlugin.configure({
component,
inputRules: [CodeBlockRules.markdown({ on: 'match' })],
shortcuts: { toggle: { keys: 'mod+alt+8' } },
});
export const CodeBlockKit = [
createCodeBlockPlugin(CodeBlockElement),
CodeHighlightPlugin.configure({
initialState: { lowlight },
}),
];'use client';
import { all, createLowlight } from 'lowlight';
import { BracesIcon, Check, CheckIcon, CopyIcon } from 'lucide-react';
import { BaseCodeBlockPlugin, CodeBlockRules } from 'platejs';
import {
CodeBlockPlugin,
CodeHighlightPlugin,
EditorElement,
type EditorElementProps,
useEditor,
useEditorReadOnly,
useElement,
} from 'platejs/react';
import * as React from 'react';
import { Button } from '@/components/ui/button';
import {
Command,
CodeBlockElement: Renders code block containers.Add the kit to your plugins:
import { createEditor } from 'platejs/react';
import { CodeBlockKit } from '@/components/editor/code-block';
const editor = createEditor({
plugins: [
// ...otherPlugins,
...CodeBlockKit,
],
});import { createEditor } from 'platejs/react';
import { CodeBlockKit } from '@/components/editor/code-block';
const editor =
CodeBlockKit keeps code in native editor DOM. Use
CodeBlockCodeMirrorElement explicitly when one code block can contain enough
text or syntax tokens to make that DOM expensive. Plate still owns the
canonical text, selection, history, decorations, and collaboration state;
CodeMirror owns the block's mounted DOM, input surface, and incremental syntax parser.
This preview contains 1,000 lines. Open the 10,000-line stress test.
'use client';
import { indentLess, indentMore, standardKeymap } from '@codemirror/commands';
import {
HighlightStyle,
indentUnit,
LanguageDescription,
syntaxHighlighting,
} from '@codemirror/language';
import { languages } from '@codemirror/language-data';
import { search, searchKeymap } from '@codemirror/search';
// oxlint-disable-next-line react-doctor/prefer-dynamic-import -- This opt-in component is the loading boundary; extensions must exist when its adapter mounts.
import { EditorState } from '@codemirror/state';
// oxlint-disable-next-line react-doctor/prefer-dynamic-import -- This opt-in component is the loading boundary; extensions must exist when its adapter mounts.
import { EditorView, keymap, runScopeHandlers } from '@codemirror/view';
import { tags } from '@lezer/highlight';
import { createCodeMirrorAdapter } from 'platejs/code-block/codemirror';
import type { CodeBlockPlugin, EditorElementProps } from 'platejs/react';
import { CodeBlockContainer } from '@/components/editor/code-block';
const syntaxColors = syntaxHighlighting(
HighlightStyle.define([
{ tag: tags.keyword, class: 'hljs-keyword' },
{ tag: [tags.string, tags.regexp], class: 'hljs-string' },
{ tag: [tags.number, tags.bool, tags.null], class: 'hljs-number' },
{ tag: tags.comment, class: 'hljs-comment' },
{ tag: tags.function(tags.variableName), class: 'hljs-title function_' },
{ tag: [tags.typeName, tags.className], class: 'hljs-type' },
{ tag: tags.propertyName, class: 'hljs-attr' },
])
);
const languageDescription = (name?: string) =>
name ? LanguageDescription.matchLanguageName(languages, name, false) : null;
const codeMirrorLanguages = [
{ label: 'Plain Text', value: 'plaintext' },
...languages.map(({ name }) => ({ label: name, value: name.toLowerCase() })),
];
const codeMirrorAdapter = createCodeMirrorAdapter({
extensions: [
syntaxColors,
indentUnit.of(' '),
EditorState.tabSize.of(2),
search({ top: true }),
keymap.of(searchKeymap),
keymap.of(
[
{ key: 'Tab', run: indentMore, shift: indentLess },
...standardKeymap,
].map((binding) => ({ ...binding, scope: 'code-block' }))
),
EditorView.domEventHandlers({
keydown(event, view) {
// Composition starts before CodeMirror observes its first text mutation.
if (event.isComposing || view.compositionStarted) return false;
return runScopeHandlers(view, event, 'code-block');
},
}),
EditorView.theme({
'&': {
backgroundColor: 'transparent',
color: 'inherit',
},
'&.cm-focused': { outline: 'none' },
'.cm-content': {
caretColor: 'currentColor',
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
fontSize: '0.875rem',
lineHeight: 'normal',
padding: '2rem 1rem 2rem 2rem',
},
'.cm-scroller': {
maxHeight: 'min(70vh, 48rem)',
overflow: 'auto',
},
'[data-code-block-model-selection]': {
backgroundColor: 'color-mix(in srgb, var(--brand) 25%, transparent)',
},
'.cm-search': {
alignItems: 'center',
backgroundColor: 'var(--background)',
borderBottom: '1px solid var(--border)',
display: 'flex',
flexWrap: 'wrap',
gap: '0.25rem',
padding: '0.375rem',
},
'.cm-search input': {
backgroundColor: 'var(--background)',
border: '1px solid var(--input)',
borderRadius: '0.375rem',
color: 'inherit',
padding: '0.25rem 0.5rem',
},
'.cm-search button': {
border: '1px solid var(--border)',
borderRadius: '0.375rem',
padding: '0.25rem 0.5rem',
},
'@media print': {
'.cm-scroller': {
maxHeight: 'none',
overflow: 'visible',
},
},
}),
],
loadLanguage(name) {
const description = languageDescription(name);
return description?.support ?? description?.load() ?? [];
},
});
export function CodeBlockCodeMirrorElement(
props: EditorElementProps<typeof CodeBlockPlugin> & {
showLanguageLabel?: boolean;
}
) {
return (
<CodeBlockContainer
elementProps={props}
languageOptions={codeMirrorLanguages}
showLanguageLabel={props.showLanguageLabel ?? true}
>
{props.slots.externalText({
adapter: codeMirrorAdapter,
ariaLabel: 'Code block',
config: { language: props.element.language },
})}
</CodeBlockContainer>
);
}'use client';
import { indentLess, indentMore, standardKeymap } from '@codemirror/commands';
import {
HighlightStyle,
indentUnit,
LanguageDescription,
syntaxHighlighting,
} from '@codemirror/language';
import { languages } from '@codemirror/language-data';
import { search, searchKeymap } from '@codemirror/search';
// oxlint-disable-next-line react-doctor/prefer-dynamic-import -- This opt-in component is the loading boundary; extensions must exist when its adapter mounts.
import { EditorState } from '@codemirror/state';
// oxlint-disable-next-line react-doctor/prefer-dynamic-import -- This opt-in component is the loading boundary; extensions must exist when its adapter mounts.
import { EditorView, keymap, runScopeHandlers } from '@codemirror/view';
import { tags } from
For an editor whose code blocks all use CodeMirror, replace the component and omit the Lowlight plugin:
import { CodeBlockPlugin, CodeHighlightPlugin } from 'platejs/react';
import {
CodeBlockCodeMirrorElement,
} from '@/components/editor/code-block-codemirror';
import { createCodeBlockPlugin } from '@/components/editor/code-block';
import { EditorKit } from '@/components/editor/plugins';
const codeBlockCodeMirrorPlugin = createCodeBlockPlugin(
CodeBlockCodeMirrorElement
);
export const LargeCodeEditorKit = EditorKit
.filter((plugin) => plugin.name !== CodeHighlightPlugin.name)
.map((plugin) =>
plugin.name === CodeBlockPlugin.name ? codeBlockCodeMirrorPlugin : plugin
);import { CodeBlockPlugin, CodeHighlightPlugin } from 'platejs/react';
import {
CodeBlockCodeMirrorElement,
} from '@/components/editor/code-block-codemirror';
import { createCodeBlockPlugin } from '@/components/editor/code-block';
import { EditorKit } from '@/components/editor/plugins';
const codeBlockCodeMirrorPlugin = createCodeBlockPlugin(
CodeBlockCodeMirrorElement
);
export const LargeCodeEditorKit = EditorKit
.filter((plugin) => plugin.name !== CodeHighlightPlugin.name)
.map((plugin) =>
plugin.name ===
Choose an explicit language from the CodeMirror picker. Parsers load on demand; plain text and unrecognized language values stay unhighlighted. CodeMirror keeps neutral range decorations, including overlapping annotations.
For native and CodeMirror views of the same editor, retain CodeHighlightPlugin
for the native view. Its syntax decorations carry data-code-block-syntax;
CodeMirror excludes those decorations and paints its own syntax. Native views
retain the full code DOM.
This choice is never automatic. Do not switch renderers at a character threshold, and do not combine a block adapter with whole-document virtualization.
CodeMirror search (Mod+F) finds offscreen code. Browser page Find can inspect
only the mounted viewport DOM. The copy button and print layout use the full
canonical code; printing temporarily renders every line.
createCodeMirrorAdapter from platejs/code-block/codemirror owns the
ExternalText projection. Create it once outside the component and pass it to
props.slots.externalText with config: { language }.
import { EditorView } from '@codemirror/view';
import { createCodeMirrorAdapter } from 'platejs/code-block/codemirror';
const adapter = createCodeMirrorAdapter({
extensions: [EditorView.lineWrapping],
});import { EditorView } from '@codemirror/view';
import { createCodeMirrorAdapter } from 'platejs/code-block/codemirror';
const adapter = createCodeMirrorAdapter({
extensions: [EditorView.lineWrapping],
});The optional entrypoint requires @codemirror/language, @codemirror/state
and @codemirror/view. extensions configures presentation, search and native
editing commands. loadLanguage(language) returns a CodeMirror extension or
a promise for one; obsolete loads are ignored after configuration changes or
disposal. Omit it for plain text. Keep CodeMirror history extensions disabled:
undo and redo use the editor's shared history.
Plate-authored text, selection, configuration, and decoration transactions bypass CodeMirror transaction filters so local extensions cannot veto canonical state. Update listeners observe a local prediction before any synchronous canonical correction. They must not synchronously dispatch another CodeMirror transaction or mutate Plate; use a transaction filter to shape the current transaction, and run later edits from an input handler or command after the listener returns.
The copied component uses two-space line indentation for Tab and syntax-aware indentation for Enter. Its language picker, search panel and theme stay in the component.
Add CodeBlockPlugin for code-block editing. Add CodeHighlightPlugin separately when you want Lowlight syntax highlighting.
import { CodeBlockPlugin } from 'platejs/react';
import { createEditor } from 'platejs/react';
const editor = createEditor({
plugins: [
// ...otherPlugins,
CodeBlockPlugin,
],
});import { CodeBlockPlugin } from 'platejs/react';
import { createEditor } from 'platejs/react';
const editor = createEditor({
Configure each capability where it is owned. CodeBlockPlugin owns the multiline text and editing behavior. CodeHighlightPlugin owns highlighting.
Basic Setup with All Languages:
import { CodeBlockRules } from 'platejs';
import { CodeBlockPlugin, CodeHighlightPlugin } from 'platejs/react';
import { all, createLowlight } from 'lowlight';
import { createEditor } from 'platejs/react';
import { CodeBlockElement } from '@/components/editor/code-block';
// Create a lowlight instance with all languages
const lowlight = createLowlight(all);
const editor = createEditor({
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.Each codeBlock contains one text child. Newline characters define physical lines; editing and highlighting derive their line behavior from text offsets.
For plain code blocks, omit CodeHighlightPlugin:
import { CodeBlockPlugin } from 'platejs/react';
import { createEditor } from 'platejs/react';
const editor = createEditor({
plugins: [CodeBlockPlugin],
});import { CodeBlockPlugin } from 'platejs/react';
import { createEditor } from 'platejs/react';
const editor = createEditor({
plugins: [CodeBlockPlugin],
});For the runtime model, see Plugin Input Rules.
Add a structural action that calls BaseCodeBlockPlugin directly. Enable it only for a contiguous sibling selection with one homogeneous active state.
import { BaseCodeBlockPlugin, PLUGINS } from 'platejs';
const turnIntoItem = {
icon: <FileCodeIcon />,
label: 'Code',
value: PLUGINS.codeBlock,
onSelect: () => {
const codeBlock = editor.plugin(BaseCodeBlockPlugin);
if (!codeBlock.installed || editor.read.view.isReadOnly()) return;
codeBlock.update.toggle();
},
};Add an item whose callback calls the installed code-block feature:
import { BaseCodeBlockPlugin, PLUGINS } from 'platejs';
const insertItem = {
icon: <FileCodeIcon />,
label: 'Code',
value: PLUGINS.codeBlock,
onSelect: () => {
const codeBlock = editor.plugin(BaseCodeBlockPlugin);
if (!codeBlock.installed || editor.read.view.isReadOnly()) return;
codeBlock.update.insert({}, { select: true });
},
};Owns the code-block schema, multiline editing behavior, input rules, shortcuts, and commands.
Produces transient syntax decorations with Lowlight. Syntax tokens are not stored as text marks. Include or omit this descriptor as one ordinary plugin-array entry.
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 physical lines by two spaces.resetBlock() unwraps the selected code block into paragraphs.selectAll() selects the containing code block.Read code-block state through the installed descriptor:
const codeBlock = editor.plugin(CodeBlockPlugin);
codeBlock.read.entry();
codeBlock.read.isEmpty();
codeBlock.read.indentDepth();const codeBlock = editor.plugin(CodeBlockPlugin);
codeBlock.read.entry();
codeBlock.read.isEmpty();
codeBlock.read.indentDepth();entry({ at? }) returns { codeBlock: [element, path] } for the containing code block, or undefined. It uses the current selection when at is omitted.isEmpty() returns whether the containing code block has no text. It returns false outside a code block.indentDepth() returns the number of leading whitespace characters on the anchor's physical line, or 0 outside a code block.import { CodeBlockRules } from 'platejs';
import { CodeBlockPlugin, CodeHighlightPlugin } from 'platejs/react';
import { all, createLowlight } from 'lowlight';
import { createEditor } from 'platejs/react';
import { CodeBlockElement } from '@/components/editor/code-block';
// Create a lowlight instance with all languages
const lowlight = createLowlight(all);
const editor = createEditor({
plugins: [
// ...otherPlugins,
CodeBlockPlugin.configure({
component: CodeBlockElement,
inputRules: [CodeBlockRules.markdown({ on: 'match' })],
shortcuts: { toggle: { keys: 'mod+alt+8' } },
}),
CodeHighlightPlugin.configure({
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 = createEditor({
plugins: [
// ...otherPlugins,
CodeBlockPlugin.configure({
component: CodeBlockElement,
inputRules: [CodeBlockRules.markdown({ on: 'match' })],
shortcuts: { toggle: { keys: 'mod+alt+8' } },
}),
CodeHighlightPlugin.configure({
initialState: {
lowlight,
defaultLanguage: 'js', // Set default language (optional)
},
}),
],
});import { BaseCodeBlockPlugin, PLUGINS } from 'platejs';
const turnIntoItem = {
icon: <FileCodeIcon />,
label: 'Code',
value: PLUGINS.codeBlock,
onSelect: () => {
const codeBlock = editor.plugin(BaseCodeBlockPlugin);
if (!codeBlock.installed || editor.read.view.isReadOnly()) return;
codeBlock.update.toggle();
},
};import { BaseCodeBlockPlugin, PLUGINS } from 'platejs';
const insertItem = {
icon: <FileCodeIcon />,
label: 'Code',
value: PLUGINS.codeBlock,
onSelect: () => {
const codeBlock = editor.plugin(BaseCodeBlockPlugin);
if (!codeBlock.installed || editor.read.view.isReadOnly()) return;
codeBlock.update.insert({}, { select: true });
},
};