Styling your editor
ProseKit is a headless editor framework, so you decide what the editor looks like. This page covers the base styles every editor needs, then shows how to layer your own CSS on top.
Looking for a ready-made toolbar, slash menu, or block handle? Skip to Components overview.
Base styles (required)
Section titled “Base styles (required)”prosekit/basic/style.css provides the structural CSS the editor relies on (focus outline, selection rendering, list bullets, etc.). Always import it.
import 'prosekit/basic/style.css'Typography (optional)
Section titled “Typography (optional)”prosekit/basic/typography.css styles headings, paragraphs, lists, quotes, and code so the document content looks polished with no extra work.
import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'Here's what the default typography looks like:
'use client'
import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import { createEditor, type NodeJSON } from 'prosekit/core'
import { ProseKit } from 'prosekit/react'
import { useMemo } from 'react'
import { sampleContent } from '../../sample/sample-doc-typography'
import { BlockHandle } from '../../ui/block-handle'
import { DropIndicator } from '../../ui/drop-indicator'
import { defineExtension } from './extension'
interface EditorProps {
initialContent?: NodeJSON
}
export default function Editor(props: EditorProps) {
const defaultContent = props.initialContent ?? sampleContent
const editor = useMemo(() => {
const extension = defineExtension()
return createEditor({ extension, defaultContent })
}, [defaultContent])
return (
<ProseKit editor={editor}>
<div className="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-[canvas] text-black dark:text-white">
<div className="relative w-full flex-1 box-border overflow-y-auto">
<div ref={editor.mount} className="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500"></div>
<BlockHandle />
<DropIndicator />
</div>
</div>
</ProseKit>
)
}import { defineBasicExtension } from 'prosekit/basic'
import { union } from 'prosekit/core'
import { defineMath } from 'prosekit/extensions/math'
import { renderKaTeXMathBlock, renderKaTeXMathInline } from '../../sample/katex'
export function defineExtension() {
return union(defineBasicExtension(), defineMath({ renderMathBlock: renderKaTeXMathBlock, renderMathInline: renderKaTeXMathInline }))
}
export type EditorExtension = ReturnType<typeof defineExtension>export { default as ExampleEditor } from './editor'import { render } from 'katex'
export function renderKaTeXMathBlock(text: string, element: HTMLElement) {
render(text, element, { displayMode: true, throwOnError: false, output: 'mathml' })
}
export function renderKaTeXMathInline(text: string, element: HTMLElement) {
render(text, element, { displayMode: false, throwOnError: false, output: 'mathml' })
}import type { NodeJSON } from 'prosekit/core'
const EULER_IDENTITY = String.raw`e^{i\pi} + 1 = 0`
const QUADRATIC_FORMULA = String.raw`x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}`
const GAUSSIAN_INTEGRAL = String.raw`\int_{-\infty}^{\infty} e^{-x^2} \, dx = \sqrt{\pi}`
export const sampleContent: NodeJSON = {
type: 'doc',
content: [
{
type: 'heading',
attrs: {
level: 1,
},
content: [
{
type: 'text',
text: 'ProseKit Typography',
},
],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'This example shows the typography styles provided by ',
},
{
type: 'text',
marks: [
{
type: 'code',
},
],
text: 'prosekit/basic/typography.css',
},
{
type: 'text',
text: '.',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Inline marks',
},
],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Text can be formatted in different ways: ',
},
{
type: 'text',
marks: [
{
type: 'bold',
},
],
text: 'bold text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'italic',
},
],
text: 'italic text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'underline',
},
],
text: 'underlined text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'strike',
},
],
text: 'strikethrough text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'code',
},
],
text: 'inline code',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'link',
attrs: {
href: 'https://example.com',
target: null,
rel: null,
},
},
],
text: 'links',
},
{
type: 'text',
text: ',',
},
{
type: 'hardBreak',
},
{
type: 'text',
text: 'and hard breaks (Shift+Enter).',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Headings',
},
],
},
{
type: 'heading',
attrs: {
level: 1,
},
content: [
{
type: 'text',
text: 'Heading 1',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Heading 2',
},
],
},
{
type: 'heading',
attrs: {
level: 3,
},
content: [
{
type: 'text',
text: 'Heading 3',
},
],
},
{
type: 'heading',
attrs: {
level: 4,
},
content: [
{
type: 'text',
text: 'Heading 4',
},
],
},
{
type: 'heading',
attrs: {
level: 5,
},
content: [
{
type: 'text',
text: 'Heading 5',
},
],
},
{
type: 'heading',
attrs: {
level: 6,
},
content: [
{
type: 'text',
text: 'Heading 6',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Lists',
},
],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Here are different types of lists:',
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Unordered list item 1',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Unordered list item 2',
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Nested item A',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Nested item B',
},
],
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'ordered',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'First ordered item',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'ordered',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Second ordered item',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'task',
order: null,
checked: true,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Completed task',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'task',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Pending task',
},
],
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Blockquotes',
},
],
},
{
type: 'blockquote',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text:
'This is a blockquote demonstrating how quoted content appears in the editor. It can span multiple lines and maintains proper formatting.',
},
],
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Code Blocks',
},
],
},
{
type: 'codeBlock',
attrs: {
language: '',
},
content: [
{
type: 'text',
text: "function example() {\n const greeting = 'Hello ProseKit!';\n console.log(greeting);\n return greeting;\n}",
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Horizontal Rule',
},
],
},
{
type: 'horizontalRule',
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Images',
},
],
},
{
type: 'image',
attrs: {
src: 'https://static.photos/blurred/640x360/42',
},
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Tables',
},
],
},
{
type: 'table',
content: [
{
type: 'tableRow',
content: [
{
type: 'tableHeaderCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Header 1',
},
],
},
],
},
{
type: 'tableHeaderCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Header 2',
},
],
},
],
},
],
},
{
type: 'tableRow',
content: [
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 1',
},
],
},
],
},
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 2',
},
],
},
],
},
],
},
{
type: 'tableRow',
content: [
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 3',
},
],
},
],
},
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 4',
},
],
},
],
},
],
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Math',
},
],
},
{
type: 'paragraph',
content: [
{ type: 'text', text: "Inline math like Euler's identity " },
{ type: 'mathInline', content: [{ type: 'text', text: EULER_IDENTITY }] },
{ type: 'text', text: ' and the quadratic formula ' },
{ type: 'mathInline', content: [{ type: 'text', text: QUADRATIC_FORMULA }] },
{ type: 'text', text: ' can appear within text.' },
],
},
{
type: 'paragraph',
content: [
{ type: 'text', text: 'Block-level equations are displayed on their own line:' },
],
},
{
type: 'mathBlock',
content: [{ type: 'text', text: GAUSSIAN_INTEGRAL }],
},
],
}'use client'
import { BlockHandleAdd, BlockHandleDraggable, BlockHandlePopup, BlockHandlePositioner, BlockHandleRoot } from 'prosekit/react/block-handle'
interface Props {
dir?: 'ltr' | 'rtl'
}
export default function BlockHandle(props: Props) {
return (
<BlockHandleRoot>
<BlockHandlePositioner
placement={props.dir === 'rtl' ? 'right' : 'left'}
className="block overflow-visible w-min h-min z-50 ease-out transition-transform duration-100 motion-reduce:transition-none"
>
<BlockHandlePopup className="flex box-border origin-(--transform-origin) transition-[opacity,scale] transition-discrete motion-reduce:transition-none duration-100 data-[state=closed]:duration-150 data-[state=closed]:opacity-0 starting:opacity-0 data-[state=closed]:scale-95 starting:scale-95">
<BlockHandleAdd className="h-6 w-6 cursor-pointer flex items-center box-border justify-center hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-400/50">
<div className="i-lucide-plus size-5 block" />
</BlockHandleAdd>
<BlockHandleDraggable className="h-6 w-5 cursor-grab flex items-center box-border justify-center hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-400/50">
<div className="i-lucide-grip-vertical size-5 block" />
</BlockHandleDraggable>
</BlockHandlePopup>
</BlockHandlePositioner>
</BlockHandleRoot>
)
}export { default as BlockHandle } from './block-handle''use client'
import { DropIndicator as BaseDropIndicator } from 'prosekit/react/drop-indicator'
export default function DropIndicator() {
return <BaseDropIndicator className="z-50 transition-all bg-blue-500" />
}export { default as DropIndicator } from './drop-indicator'import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import { useMemo } from 'preact/hooks'
import { createEditor, type NodeJSON } from 'prosekit/core'
import { ProseKit } from 'prosekit/preact'
import { sampleContent } from '../../sample/sample-doc-typography'
import { BlockHandle } from '../../ui/block-handle'
import { DropIndicator } from '../../ui/drop-indicator'
import { defineExtension } from './extension'
interface EditorProps {
initialContent?: NodeJSON
}
export default function Editor(props: EditorProps) {
const defaultContent = props.initialContent ?? sampleContent
const editor = useMemo(() => {
const extension = defineExtension()
return createEditor({ extension, defaultContent })
}, [defaultContent])
return (
<ProseKit editor={editor}>
<div className="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-[canvas] text-black dark:text-white">
<div className="relative w-full flex-1 box-border overflow-y-auto">
<div ref={editor.mount} className="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500"></div>
<BlockHandle />
<DropIndicator />
</div>
</div>
</ProseKit>
)
}import { defineBasicExtension } from 'prosekit/basic'
import { union } from 'prosekit/core'
import { defineMath } from 'prosekit/extensions/math'
import { renderKaTeXMathBlock, renderKaTeXMathInline } from '../../sample/katex'
export function defineExtension() {
return union(defineBasicExtension(), defineMath({ renderMathBlock: renderKaTeXMathBlock, renderMathInline: renderKaTeXMathInline }))
}
export type EditorExtension = ReturnType<typeof defineExtension>export { default as ExampleEditor } from './editor'import { render } from 'katex'
export function renderKaTeXMathBlock(text: string, element: HTMLElement) {
render(text, element, { displayMode: true, throwOnError: false, output: 'mathml' })
}
export function renderKaTeXMathInline(text: string, element: HTMLElement) {
render(text, element, { displayMode: false, throwOnError: false, output: 'mathml' })
}import type { NodeJSON } from 'prosekit/core'
const EULER_IDENTITY = String.raw`e^{i\pi} + 1 = 0`
const QUADRATIC_FORMULA = String.raw`x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}`
const GAUSSIAN_INTEGRAL = String.raw`\int_{-\infty}^{\infty} e^{-x^2} \, dx = \sqrt{\pi}`
export const sampleContent: NodeJSON = {
type: 'doc',
content: [
{
type: 'heading',
attrs: {
level: 1,
},
content: [
{
type: 'text',
text: 'ProseKit Typography',
},
],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'This example shows the typography styles provided by ',
},
{
type: 'text',
marks: [
{
type: 'code',
},
],
text: 'prosekit/basic/typography.css',
},
{
type: 'text',
text: '.',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Inline marks',
},
],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Text can be formatted in different ways: ',
},
{
type: 'text',
marks: [
{
type: 'bold',
},
],
text: 'bold text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'italic',
},
],
text: 'italic text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'underline',
},
],
text: 'underlined text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'strike',
},
],
text: 'strikethrough text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'code',
},
],
text: 'inline code',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'link',
attrs: {
href: 'https://example.com',
target: null,
rel: null,
},
},
],
text: 'links',
},
{
type: 'text',
text: ',',
},
{
type: 'hardBreak',
},
{
type: 'text',
text: 'and hard breaks (Shift+Enter).',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Headings',
},
],
},
{
type: 'heading',
attrs: {
level: 1,
},
content: [
{
type: 'text',
text: 'Heading 1',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Heading 2',
},
],
},
{
type: 'heading',
attrs: {
level: 3,
},
content: [
{
type: 'text',
text: 'Heading 3',
},
],
},
{
type: 'heading',
attrs: {
level: 4,
},
content: [
{
type: 'text',
text: 'Heading 4',
},
],
},
{
type: 'heading',
attrs: {
level: 5,
},
content: [
{
type: 'text',
text: 'Heading 5',
},
],
},
{
type: 'heading',
attrs: {
level: 6,
},
content: [
{
type: 'text',
text: 'Heading 6',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Lists',
},
],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Here are different types of lists:',
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Unordered list item 1',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Unordered list item 2',
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Nested item A',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Nested item B',
},
],
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'ordered',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'First ordered item',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'ordered',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Second ordered item',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'task',
order: null,
checked: true,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Completed task',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'task',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Pending task',
},
],
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Blockquotes',
},
],
},
{
type: 'blockquote',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text:
'This is a blockquote demonstrating how quoted content appears in the editor. It can span multiple lines and maintains proper formatting.',
},
],
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Code Blocks',
},
],
},
{
type: 'codeBlock',
attrs: {
language: '',
},
content: [
{
type: 'text',
text: "function example() {\n const greeting = 'Hello ProseKit!';\n console.log(greeting);\n return greeting;\n}",
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Horizontal Rule',
},
],
},
{
type: 'horizontalRule',
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Images',
},
],
},
{
type: 'image',
attrs: {
src: 'https://static.photos/blurred/640x360/42',
},
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Tables',
},
],
},
{
type: 'table',
content: [
{
type: 'tableRow',
content: [
{
type: 'tableHeaderCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Header 1',
},
],
},
],
},
{
type: 'tableHeaderCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Header 2',
},
],
},
],
},
],
},
{
type: 'tableRow',
content: [
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 1',
},
],
},
],
},
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 2',
},
],
},
],
},
],
},
{
type: 'tableRow',
content: [
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 3',
},
],
},
],
},
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 4',
},
],
},
],
},
],
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Math',
},
],
},
{
type: 'paragraph',
content: [
{ type: 'text', text: "Inline math like Euler's identity " },
{ type: 'mathInline', content: [{ type: 'text', text: EULER_IDENTITY }] },
{ type: 'text', text: ' and the quadratic formula ' },
{ type: 'mathInline', content: [{ type: 'text', text: QUADRATIC_FORMULA }] },
{ type: 'text', text: ' can appear within text.' },
],
},
{
type: 'paragraph',
content: [
{ type: 'text', text: 'Block-level equations are displayed on their own line:' },
],
},
{
type: 'mathBlock',
content: [{ type: 'text', text: GAUSSIAN_INTEGRAL }],
},
],
}import {
BlockHandleAdd,
BlockHandleDraggable,
BlockHandlePopup,
BlockHandlePositioner,
BlockHandleRoot,
} from 'prosekit/preact/block-handle'
interface Props {
dir?: 'ltr' | 'rtl'
}
export default function BlockHandle(props: Props) {
return (
<BlockHandleRoot>
<BlockHandlePositioner
placement={props.dir === 'rtl' ? 'right' : 'left'}
className="block overflow-visible w-min h-min z-50 ease-out transition-transform duration-100 motion-reduce:transition-none"
>
<BlockHandlePopup className="flex box-border origin-(--transform-origin) transition-[opacity,scale] transition-discrete motion-reduce:transition-none duration-100 data-[state=closed]:duration-150 data-[state=closed]:opacity-0 starting:opacity-0 data-[state=closed]:scale-95 starting:scale-95">
<BlockHandleAdd className="h-6 w-6 cursor-pointer flex items-center box-border justify-center hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-400/50">
<div className="i-lucide-plus size-5 block" />
</BlockHandleAdd>
<BlockHandleDraggable className="h-6 w-5 cursor-grab flex items-center box-border justify-center hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-400/50">
<div className="i-lucide-grip-vertical size-5 block" />
</BlockHandleDraggable>
</BlockHandlePopup>
</BlockHandlePositioner>
</BlockHandleRoot>
)
}export { default as BlockHandle } from './block-handle'import { DropIndicator as BaseDropIndicator } from 'prosekit/preact/drop-indicator'
export default function DropIndicator() {
return <BaseDropIndicator className="z-50 transition-all bg-blue-500" />
}export { default as DropIndicator } from './drop-indicator'import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import { createEditor, type NodeJSON } from 'prosekit/core'
import { ProseKit } from 'prosekit/solid'
import type { JSX } from 'solid-js'
import { sampleContent } from '../../sample/sample-doc-typography'
import { BlockHandle } from '../../ui/block-handle'
import { DropIndicator } from '../../ui/drop-indicator'
import { defineExtension } from './extension'
interface EditorProps {
initialContent?: NodeJSON
}
export default function Editor(props: EditorProps): JSX.Element {
const defaultContent = props.initialContent ?? sampleContent
const extension = defineExtension()
const editor = createEditor({ extension, defaultContent })
return (
<ProseKit editor={editor}>
<div class="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-[canvas] text-black dark:text-white">
<div class="relative w-full flex-1 box-border overflow-y-auto">
<div ref={editor.mount} class="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500"></div>
<BlockHandle />
<DropIndicator />
</div>
</div>
</ProseKit>
)
}import { defineBasicExtension } from 'prosekit/basic'
import { union } from 'prosekit/core'
import { defineMath } from 'prosekit/extensions/math'
import { renderKaTeXMathBlock, renderKaTeXMathInline } from '../../sample/katex'
export function defineExtension() {
return union(defineBasicExtension(), defineMath({ renderMathBlock: renderKaTeXMathBlock, renderMathInline: renderKaTeXMathInline }))
}
export type EditorExtension = ReturnType<typeof defineExtension>export { default as ExampleEditor } from './editor'import { render } from 'katex'
export function renderKaTeXMathBlock(text: string, element: HTMLElement) {
render(text, element, { displayMode: true, throwOnError: false, output: 'mathml' })
}
export function renderKaTeXMathInline(text: string, element: HTMLElement) {
render(text, element, { displayMode: false, throwOnError: false, output: 'mathml' })
}import type { NodeJSON } from 'prosekit/core'
const EULER_IDENTITY = String.raw`e^{i\pi} + 1 = 0`
const QUADRATIC_FORMULA = String.raw`x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}`
const GAUSSIAN_INTEGRAL = String.raw`\int_{-\infty}^{\infty} e^{-x^2} \, dx = \sqrt{\pi}`
export const sampleContent: NodeJSON = {
type: 'doc',
content: [
{
type: 'heading',
attrs: {
level: 1,
},
content: [
{
type: 'text',
text: 'ProseKit Typography',
},
],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'This example shows the typography styles provided by ',
},
{
type: 'text',
marks: [
{
type: 'code',
},
],
text: 'prosekit/basic/typography.css',
},
{
type: 'text',
text: '.',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Inline marks',
},
],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Text can be formatted in different ways: ',
},
{
type: 'text',
marks: [
{
type: 'bold',
},
],
text: 'bold text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'italic',
},
],
text: 'italic text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'underline',
},
],
text: 'underlined text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'strike',
},
],
text: 'strikethrough text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'code',
},
],
text: 'inline code',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'link',
attrs: {
href: 'https://example.com',
target: null,
rel: null,
},
},
],
text: 'links',
},
{
type: 'text',
text: ',',
},
{
type: 'hardBreak',
},
{
type: 'text',
text: 'and hard breaks (Shift+Enter).',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Headings',
},
],
},
{
type: 'heading',
attrs: {
level: 1,
},
content: [
{
type: 'text',
text: 'Heading 1',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Heading 2',
},
],
},
{
type: 'heading',
attrs: {
level: 3,
},
content: [
{
type: 'text',
text: 'Heading 3',
},
],
},
{
type: 'heading',
attrs: {
level: 4,
},
content: [
{
type: 'text',
text: 'Heading 4',
},
],
},
{
type: 'heading',
attrs: {
level: 5,
},
content: [
{
type: 'text',
text: 'Heading 5',
},
],
},
{
type: 'heading',
attrs: {
level: 6,
},
content: [
{
type: 'text',
text: 'Heading 6',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Lists',
},
],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Here are different types of lists:',
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Unordered list item 1',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Unordered list item 2',
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Nested item A',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Nested item B',
},
],
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'ordered',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'First ordered item',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'ordered',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Second ordered item',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'task',
order: null,
checked: true,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Completed task',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'task',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Pending task',
},
],
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Blockquotes',
},
],
},
{
type: 'blockquote',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text:
'This is a blockquote demonstrating how quoted content appears in the editor. It can span multiple lines and maintains proper formatting.',
},
],
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Code Blocks',
},
],
},
{
type: 'codeBlock',
attrs: {
language: '',
},
content: [
{
type: 'text',
text: "function example() {\n const greeting = 'Hello ProseKit!';\n console.log(greeting);\n return greeting;\n}",
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Horizontal Rule',
},
],
},
{
type: 'horizontalRule',
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Images',
},
],
},
{
type: 'image',
attrs: {
src: 'https://static.photos/blurred/640x360/42',
},
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Tables',
},
],
},
{
type: 'table',
content: [
{
type: 'tableRow',
content: [
{
type: 'tableHeaderCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Header 1',
},
],
},
],
},
{
type: 'tableHeaderCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Header 2',
},
],
},
],
},
],
},
{
type: 'tableRow',
content: [
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 1',
},
],
},
],
},
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 2',
},
],
},
],
},
],
},
{
type: 'tableRow',
content: [
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 3',
},
],
},
],
},
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 4',
},
],
},
],
},
],
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Math',
},
],
},
{
type: 'paragraph',
content: [
{ type: 'text', text: "Inline math like Euler's identity " },
{ type: 'mathInline', content: [{ type: 'text', text: EULER_IDENTITY }] },
{ type: 'text', text: ' and the quadratic formula ' },
{ type: 'mathInline', content: [{ type: 'text', text: QUADRATIC_FORMULA }] },
{ type: 'text', text: ' can appear within text.' },
],
},
{
type: 'paragraph',
content: [
{ type: 'text', text: 'Block-level equations are displayed on their own line:' },
],
},
{
type: 'mathBlock',
content: [{ type: 'text', text: GAUSSIAN_INTEGRAL }],
},
],
}import { BlockHandleAdd, BlockHandleDraggable, BlockHandlePopup, BlockHandlePositioner, BlockHandleRoot } from 'prosekit/solid/block-handle'
import type { JSX } from 'solid-js'
interface Props {
dir?: 'ltr' | 'rtl'
}
export default function BlockHandle(props: Props): JSX.Element {
return (
<BlockHandleRoot>
<BlockHandlePositioner
placement={props.dir === 'rtl' ? 'right' : 'left'}
class="block overflow-visible w-min h-min z-50 ease-out transition-transform duration-100 motion-reduce:transition-none"
>
<BlockHandlePopup class="flex box-border origin-(--transform-origin) transition-[opacity,scale] transition-discrete motion-reduce:transition-none duration-100 data-[state=closed]:duration-150 data-[state=closed]:opacity-0 starting:opacity-0 data-[state=closed]:scale-95 starting:scale-95">
<BlockHandleAdd class="h-6 w-6 cursor-pointer flex items-center box-border justify-center hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-400/50">
<div class="i-lucide-plus size-5 block" />
</BlockHandleAdd>
<BlockHandleDraggable class="h-6 w-5 cursor-grab flex items-center box-border justify-center hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-400/50">
<div class="i-lucide-grip-vertical size-5 block" />
</BlockHandleDraggable>
</BlockHandlePopup>
</BlockHandlePositioner>
</BlockHandleRoot>
)
}export { default as BlockHandle } from './block-handle'import { DropIndicator as BaseDropIndicator } from 'prosekit/solid/drop-indicator'
import type { JSX } from 'solid-js'
export default function DropIndicator(): JSX.Element {
return <BaseDropIndicator class="z-50 transition-all bg-blue-500" />
}export { default as DropIndicator } from './drop-indicator'<script lang="ts">
import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import { createEditor, type NodeJSON } from 'prosekit/core'
import { ProseKit } from 'prosekit/svelte'
import { untrack } from 'svelte'
import { sampleContent } from '../../sample/sample-doc-typography'
import { BlockHandle } from '../../ui/block-handle'
import { DropIndicator } from '../../ui/drop-indicator'
import { defineExtension } from './extension'
const props: {
initialContent?: NodeJSON
} = $props()
const extension = defineExtension()
const defaultContent = untrack(() => props.initialContent ?? sampleContent)
const editor = createEditor({ extension, defaultContent })
</script>
<ProseKit {editor}>
<div class="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-[canvas] text-black dark:text-white">
<div class="relative w-full flex-1 box-border overflow-y-auto">
<div {@attach editor.mount} class="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500"></div>
<BlockHandle />
<DropIndicator />
</div>
</div>
</ProseKit>import { defineBasicExtension } from 'prosekit/basic'
import { union } from 'prosekit/core'
import { defineMath } from 'prosekit/extensions/math'
import { renderKaTeXMathBlock, renderKaTeXMathInline } from '../../sample/katex'
export function defineExtension() {
return union(defineBasicExtension(), defineMath({ renderMathBlock: renderKaTeXMathBlock, renderMathInline: renderKaTeXMathInline }))
}
export type EditorExtension = ReturnType<typeof defineExtension>export { default as ExampleEditor } from './editor.svelte'import { render } from 'katex'
export function renderKaTeXMathBlock(text: string, element: HTMLElement) {
render(text, element, { displayMode: true, throwOnError: false, output: 'mathml' })
}
export function renderKaTeXMathInline(text: string, element: HTMLElement) {
render(text, element, { displayMode: false, throwOnError: false, output: 'mathml' })
}import type { NodeJSON } from 'prosekit/core'
const EULER_IDENTITY = String.raw`e^{i\pi} + 1 = 0`
const QUADRATIC_FORMULA = String.raw`x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}`
const GAUSSIAN_INTEGRAL = String.raw`\int_{-\infty}^{\infty} e^{-x^2} \, dx = \sqrt{\pi}`
export const sampleContent: NodeJSON = {
type: 'doc',
content: [
{
type: 'heading',
attrs: {
level: 1,
},
content: [
{
type: 'text',
text: 'ProseKit Typography',
},
],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'This example shows the typography styles provided by ',
},
{
type: 'text',
marks: [
{
type: 'code',
},
],
text: 'prosekit/basic/typography.css',
},
{
type: 'text',
text: '.',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Inline marks',
},
],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Text can be formatted in different ways: ',
},
{
type: 'text',
marks: [
{
type: 'bold',
},
],
text: 'bold text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'italic',
},
],
text: 'italic text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'underline',
},
],
text: 'underlined text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'strike',
},
],
text: 'strikethrough text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'code',
},
],
text: 'inline code',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'link',
attrs: {
href: 'https://example.com',
target: null,
rel: null,
},
},
],
text: 'links',
},
{
type: 'text',
text: ',',
},
{
type: 'hardBreak',
},
{
type: 'text',
text: 'and hard breaks (Shift+Enter).',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Headings',
},
],
},
{
type: 'heading',
attrs: {
level: 1,
},
content: [
{
type: 'text',
text: 'Heading 1',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Heading 2',
},
],
},
{
type: 'heading',
attrs: {
level: 3,
},
content: [
{
type: 'text',
text: 'Heading 3',
},
],
},
{
type: 'heading',
attrs: {
level: 4,
},
content: [
{
type: 'text',
text: 'Heading 4',
},
],
},
{
type: 'heading',
attrs: {
level: 5,
},
content: [
{
type: 'text',
text: 'Heading 5',
},
],
},
{
type: 'heading',
attrs: {
level: 6,
},
content: [
{
type: 'text',
text: 'Heading 6',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Lists',
},
],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Here are different types of lists:',
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Unordered list item 1',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Unordered list item 2',
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Nested item A',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Nested item B',
},
],
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'ordered',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'First ordered item',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'ordered',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Second ordered item',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'task',
order: null,
checked: true,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Completed task',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'task',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Pending task',
},
],
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Blockquotes',
},
],
},
{
type: 'blockquote',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text:
'This is a blockquote demonstrating how quoted content appears in the editor. It can span multiple lines and maintains proper formatting.',
},
],
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Code Blocks',
},
],
},
{
type: 'codeBlock',
attrs: {
language: '',
},
content: [
{
type: 'text',
text: "function example() {\n const greeting = 'Hello ProseKit!';\n console.log(greeting);\n return greeting;\n}",
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Horizontal Rule',
},
],
},
{
type: 'horizontalRule',
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Images',
},
],
},
{
type: 'image',
attrs: {
src: 'https://static.photos/blurred/640x360/42',
},
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Tables',
},
],
},
{
type: 'table',
content: [
{
type: 'tableRow',
content: [
{
type: 'tableHeaderCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Header 1',
},
],
},
],
},
{
type: 'tableHeaderCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Header 2',
},
],
},
],
},
],
},
{
type: 'tableRow',
content: [
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 1',
},
],
},
],
},
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 2',
},
],
},
],
},
],
},
{
type: 'tableRow',
content: [
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 3',
},
],
},
],
},
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 4',
},
],
},
],
},
],
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Math',
},
],
},
{
type: 'paragraph',
content: [
{ type: 'text', text: "Inline math like Euler's identity " },
{ type: 'mathInline', content: [{ type: 'text', text: EULER_IDENTITY }] },
{ type: 'text', text: ' and the quadratic formula ' },
{ type: 'mathInline', content: [{ type: 'text', text: QUADRATIC_FORMULA }] },
{ type: 'text', text: ' can appear within text.' },
],
},
{
type: 'paragraph',
content: [
{ type: 'text', text: 'Block-level equations are displayed on their own line:' },
],
},
{
type: 'mathBlock',
content: [{ type: 'text', text: GAUSSIAN_INTEGRAL }],
},
],
}<script lang="ts">
import {
BlockHandleAdd,
BlockHandleDraggable,
BlockHandlePopup,
BlockHandlePositioner,
BlockHandleRoot,
} from 'prosekit/svelte/block-handle'
interface Props {
dir?: 'ltr' | 'rtl'
}
const props: Props = $props()
</script>
<BlockHandleRoot>
<BlockHandlePositioner placement={props.dir === 'rtl' ? 'right' : 'left'} class="block overflow-visible w-min h-min z-50 ease-out transition-transform duration-100 motion-reduce:transition-none">
<BlockHandlePopup class="flex box-border origin-(--transform-origin) transition-[opacity,scale] transition-discrete motion-reduce:transition-none duration-100 data-[state=closed]:duration-150 data-[state=closed]:opacity-0 starting:opacity-0 data-[state=closed]:scale-95 starting:scale-95">
<BlockHandleAdd class="h-6 w-6 cursor-pointer flex items-center box-border justify-center hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-400/50">
<div class="i-lucide-plus size-5 block"></div>
</BlockHandleAdd>
<BlockHandleDraggable class="h-6 w-5 cursor-grab flex items-center box-border justify-center hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-400/50">
<div class="i-lucide-grip-vertical size-5 block"></div>
</BlockHandleDraggable>
</BlockHandlePopup>
</BlockHandlePositioner>
</BlockHandleRoot>export { default as BlockHandle } from './block-handle.svelte'<script lang="ts">
import { DropIndicator as BaseDropIndicator } from 'prosekit/svelte/drop-indicator'
</script>
<BaseDropIndicator class="z-50 transition-all bg-blue-500" />export { default as DropIndicator } from './drop-indicator.svelte'<script setup lang="ts">
import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import { createEditor, type NodeJSON } from 'prosekit/core'
import { ProseKit } from 'prosekit/vue'
import { sampleContent } from '../../sample/sample-doc-typography'
import { BlockHandle } from '../../ui/block-handle'
import { DropIndicator } from '../../ui/drop-indicator'
import { defineExtension } from './extension'
const props = defineProps<{
initialContent?: NodeJSON
}>()
const extension = defineExtension()
const defaultContent = props.initialContent ?? sampleContent
const editor = createEditor({ extension, defaultContent })
</script>
<template>
<ProseKit :editor="editor">
<div class="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-[canvas] text-black dark:text-white">
<div class="relative w-full flex-1 box-border overflow-y-auto">
<div :ref="(el) => editor.mount(el as HTMLElement | null)" class="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500" />
<BlockHandle />
<DropIndicator />
</div>
</div>
</ProseKit>
</template>import { defineBasicExtension } from 'prosekit/basic'
import { union } from 'prosekit/core'
import { defineMath } from 'prosekit/extensions/math'
import { renderKaTeXMathBlock, renderKaTeXMathInline } from '../../sample/katex'
export function defineExtension() {
return union(defineBasicExtension(), defineMath({ renderMathBlock: renderKaTeXMathBlock, renderMathInline: renderKaTeXMathInline }))
}
export type EditorExtension = ReturnType<typeof defineExtension>export { default as ExampleEditor } from './editor.vue'import { render } from 'katex'
export function renderKaTeXMathBlock(text: string, element: HTMLElement) {
render(text, element, { displayMode: true, throwOnError: false, output: 'mathml' })
}
export function renderKaTeXMathInline(text: string, element: HTMLElement) {
render(text, element, { displayMode: false, throwOnError: false, output: 'mathml' })
}import type { NodeJSON } from 'prosekit/core'
const EULER_IDENTITY = String.raw`e^{i\pi} + 1 = 0`
const QUADRATIC_FORMULA = String.raw`x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}`
const GAUSSIAN_INTEGRAL = String.raw`\int_{-\infty}^{\infty} e^{-x^2} \, dx = \sqrt{\pi}`
export const sampleContent: NodeJSON = {
type: 'doc',
content: [
{
type: 'heading',
attrs: {
level: 1,
},
content: [
{
type: 'text',
text: 'ProseKit Typography',
},
],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'This example shows the typography styles provided by ',
},
{
type: 'text',
marks: [
{
type: 'code',
},
],
text: 'prosekit/basic/typography.css',
},
{
type: 'text',
text: '.',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Inline marks',
},
],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Text can be formatted in different ways: ',
},
{
type: 'text',
marks: [
{
type: 'bold',
},
],
text: 'bold text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'italic',
},
],
text: 'italic text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'underline',
},
],
text: 'underlined text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'strike',
},
],
text: 'strikethrough text',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'code',
},
],
text: 'inline code',
},
{
type: 'text',
text: ', ',
},
{
type: 'text',
marks: [
{
type: 'link',
attrs: {
href: 'https://example.com',
target: null,
rel: null,
},
},
],
text: 'links',
},
{
type: 'text',
text: ',',
},
{
type: 'hardBreak',
},
{
type: 'text',
text: 'and hard breaks (Shift+Enter).',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Headings',
},
],
},
{
type: 'heading',
attrs: {
level: 1,
},
content: [
{
type: 'text',
text: 'Heading 1',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Heading 2',
},
],
},
{
type: 'heading',
attrs: {
level: 3,
},
content: [
{
type: 'text',
text: 'Heading 3',
},
],
},
{
type: 'heading',
attrs: {
level: 4,
},
content: [
{
type: 'text',
text: 'Heading 4',
},
],
},
{
type: 'heading',
attrs: {
level: 5,
},
content: [
{
type: 'text',
text: 'Heading 5',
},
],
},
{
type: 'heading',
attrs: {
level: 6,
},
content: [
{
type: 'text',
text: 'Heading 6',
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Lists',
},
],
},
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Here are different types of lists:',
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Unordered list item 1',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Unordered list item 2',
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Nested item A',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'bullet',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Nested item B',
},
],
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'ordered',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'First ordered item',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'ordered',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Second ordered item',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'task',
order: null,
checked: true,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Completed task',
},
],
},
],
},
{
type: 'list',
attrs: {
kind: 'task',
order: null,
checked: false,
collapsed: false,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Pending task',
},
],
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Blockquotes',
},
],
},
{
type: 'blockquote',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text:
'This is a blockquote demonstrating how quoted content appears in the editor. It can span multiple lines and maintains proper formatting.',
},
],
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Code Blocks',
},
],
},
{
type: 'codeBlock',
attrs: {
language: '',
},
content: [
{
type: 'text',
text: "function example() {\n const greeting = 'Hello ProseKit!';\n console.log(greeting);\n return greeting;\n}",
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Horizontal Rule',
},
],
},
{
type: 'horizontalRule',
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Images',
},
],
},
{
type: 'image',
attrs: {
src: 'https://static.photos/blurred/640x360/42',
},
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Tables',
},
],
},
{
type: 'table',
content: [
{
type: 'tableRow',
content: [
{
type: 'tableHeaderCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Header 1',
},
],
},
],
},
{
type: 'tableHeaderCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Header 2',
},
],
},
],
},
],
},
{
type: 'tableRow',
content: [
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 1',
},
],
},
],
},
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 2',
},
],
},
],
},
],
},
{
type: 'tableRow',
content: [
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 3',
},
],
},
],
},
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: null,
},
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Cell 4',
},
],
},
],
},
],
},
],
},
{
type: 'heading',
attrs: {
level: 2,
},
content: [
{
type: 'text',
text: 'Math',
},
],
},
{
type: 'paragraph',
content: [
{ type: 'text', text: "Inline math like Euler's identity " },
{ type: 'mathInline', content: [{ type: 'text', text: EULER_IDENTITY }] },
{ type: 'text', text: ' and the quadratic formula ' },
{ type: 'mathInline', content: [{ type: 'text', text: QUADRATIC_FORMULA }] },
{ type: 'text', text: ' can appear within text.' },
],
},
{
type: 'paragraph',
content: [
{ type: 'text', text: 'Block-level equations are displayed on their own line:' },
],
},
{
type: 'mathBlock',
content: [{ type: 'text', text: GAUSSIAN_INTEGRAL }],
},
],
}<script setup lang="ts">
import { BlockHandleAdd, BlockHandleDraggable, BlockHandlePopup, BlockHandlePositioner, BlockHandleRoot } from 'prosekit/vue/block-handle'
interface Props {
dir?: 'ltr' | 'rtl'
}
const props = defineProps<Props>()
</script>
<template>
<BlockHandleRoot>
<BlockHandlePositioner :placement="props.dir === 'rtl' ? 'right' : 'left'" class="block overflow-visible w-min h-min z-50 ease-out transition-transform duration-100 motion-reduce:transition-none">
<BlockHandlePopup class="flex box-border origin-(--transform-origin) transition-[opacity,scale] transition-discrete motion-reduce:transition-none duration-100 data-[state=closed]:duration-150 data-[state=closed]:opacity-0 starting:opacity-0 data-[state=closed]:scale-95 starting:scale-95">
<BlockHandleAdd class="h-6 w-6 cursor-pointer flex items-center box-border justify-center hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-400/50">
<div class="i-lucide-plus size-5 block" />
</BlockHandleAdd>
<BlockHandleDraggable class="h-6 w-5 cursor-grab flex items-center box-border justify-center hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-400/50">
<div class="i-lucide-grip-vertical size-5 block" />
</BlockHandleDraggable>
</BlockHandlePopup>
</BlockHandlePositioner>
</BlockHandleRoot>
</template>export { default as BlockHandle } from './block-handle.vue'<script setup lang="ts">
import { DropIndicator as BaseDropIndicator } from 'prosekit/vue/drop-indicator'
</script>
<template>
<BaseDropIndicator class="z-50 transition-all bg-blue-500" />
</template>export { default as DropIndicator } from './drop-indicator.vue'Per-extension stylesheets
Section titled “Per-extension stylesheets”Some extensions (lists, tables, placeholders, search, gap-cursor, collaboration, page) ship a small CSS file you import alongside the extension. Each extension page tells you whether to import one. For example, the list page calls out prosekit/extensions/list/style.css.
Custom CSS
Section titled “Custom CSS”The editor mounts on whatever element you pass to editor.mount(...). Style its container however you like:
.my-editor {
min-height: 200px;
padding: 16px;
border: 1px solid var(--color-border);
border-radius: 8px;
outline: none;
}
.my-editor:focus-within {
border-color: var(--color-accent);
}To target nodes inside the document, use standard HTML selectors (p, h1, ul, etc.). The basic stylesheet doesn't add any wrapper class.