Styling Your Editor
ProseKit provides a headless editor framework that gives you complete control over your editor's appearance. This guide will walk you through the various ways to style your ProseKit editor.
Basic Styles
Section titled “Basic Styles”ProseKit requires some basic styles to function properly. Import prosekit/basic/style.css in your project to include these styles.
// Import the basic styles (required)
import 'prosekit/basic/style.css'Typography Styles
Section titled “Typography Styles”ProseKit provides an optional set of typography styles that make your editor content look polished with minimal effort.
prosekit/basic/typography.css provides a beautiful set of default styles for headings, paragraphs, lists, and other elements:
// Import both basic and typography styles
import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'Here's what the default typography looks like:
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-white dark:bg-gray-950 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>'use client'
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, BlockHandlePopover } from 'prosekit/react/block-handle'
interface Props {
dir?: 'ltr' | 'rtl'
}
export default function BlockHandle(props: Props) {
return (
<BlockHandlePopover
placement={props.dir === 'rtl' ? 'right' : 'left'}
className="flex items-center flex-row box-border justify-center transition border-0 [&:not([data-state])]:hidden will-change-transform motion-safe:data-[state=open]:animate-in motion-safe:data-[state=closed]:animate-out motion-safe:data-[state=open]:fade-in-0 motion-safe:data-[state=closed]:fade-out-0 motion-safe:data-[state=open]:zoom-in-95 motion-safe:data-[state=closed]:zoom-out-95 motion-safe:data-[state=open]:animate-duration-150 motion-safe:data-[state=closed]:animate-duration-200"
>
<BlockHandleAdd className="flex items-center box-border justify-center h-[1.5em] w-[1.5em] hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-500/50 cursor-pointer">
<div className="i-lucide-plus size-5 block" />
</BlockHandleAdd>
<BlockHandleDraggable className="flex items-center box-border justify-center h-[1.5em] w-[1.2em] hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-500/50 cursor-grab">
<div className="i-lucide-grip-vertical size-5 block" />
</BlockHandleDraggable>
</BlockHandlePopover>
)
}export { default as BlockHandle } from './block-handle'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-white dark:bg-gray-950 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, BlockHandlePopover } from 'prosekit/preact/block-handle'
interface Props {
dir?: 'ltr' | 'rtl'
}
export default function BlockHandle(props: Props) {
return (
<BlockHandlePopover
placement={props.dir === 'rtl' ? 'right' : 'left'}
className="flex items-center flex-row box-border justify-center transition border-0 [&:not([data-state])]:hidden will-change-transform motion-safe:data-[state=open]:animate-in motion-safe:data-[state=closed]:animate-out motion-safe:data-[state=open]:fade-in-0 motion-safe:data-[state=closed]:fade-out-0 motion-safe:data-[state=open]:zoom-in-95 motion-safe:data-[state=closed]:zoom-out-95 motion-safe:data-[state=open]:animate-duration-150 motion-safe:data-[state=closed]:animate-duration-200"
>
<BlockHandleAdd className="flex items-center box-border justify-center h-[1.5em] w-[1.5em] hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-500/50 cursor-pointer">
<div className="i-lucide-plus size-5 block" />
</BlockHandleAdd>
<BlockHandleDraggable className="flex items-center box-border justify-center h-[1.5em] w-[1.2em] hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-500/50 cursor-grab">
<div className="i-lucide-grip-vertical size-5 block" />
</BlockHandleDraggable>
</BlockHandlePopover>
)
}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-white dark:bg-gray-950 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, BlockHandlePopover } 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 (
<BlockHandlePopover
placement={props.dir === 'rtl' ? 'right' : 'left'}
class="flex items-center flex-row box-border justify-center transition border-0 [&:not([data-state])]:hidden will-change-transform motion-safe:data-[state=open]:animate-in motion-safe:data-[state=closed]:animate-out motion-safe:data-[state=open]:fade-in-0 motion-safe:data-[state=closed]:fade-out-0 motion-safe:data-[state=open]:zoom-in-95 motion-safe:data-[state=closed]:zoom-out-95 motion-safe:data-[state=open]:animate-duration-150 motion-safe:data-[state=closed]:animate-duration-200"
>
<BlockHandleAdd class="flex items-center box-border justify-center h-[1.5em] w-[1.5em] hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-500/50 cursor-pointer">
<div class="i-lucide-plus size-5 block" />
</BlockHandleAdd>
<BlockHandleDraggable class="flex items-center box-border justify-center h-[1.5em] w-[1.2em] hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-500/50 cursor-grab">
<div class="i-lucide-grip-vertical size-5 block" />
</BlockHandleDraggable>
</BlockHandlePopover>
)
}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 { 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 = 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-white dark:bg-gray-950 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, BlockHandlePopover } from 'prosekit/svelte/block-handle'
interface Props {
dir?: 'ltr' | 'rtl'
}
const props: Props = $props()
</script>
<BlockHandlePopover
placement={props.dir === 'rtl' ? 'right' : 'left'}
class="flex items-center flex-row box-border justify-center transition border-0 [&:not([data-state])]:hidden will-change-transform motion-safe:data-[state=open]:animate-in motion-safe:data-[state=closed]:animate-out motion-safe:data-[state=open]:fade-in-0 motion-safe:data-[state=closed]:fade-out-0 motion-safe:data-[state=open]:zoom-in-95 motion-safe:data-[state=closed]:zoom-out-95 motion-safe:data-[state=open]:animate-duration-150 motion-safe:data-[state=closed]:animate-duration-200"
>
<BlockHandleAdd class="flex items-center box-border justify-center h-[1.5em] w-[1.5em] hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-500/50 cursor-pointer">
<div class="i-lucide-plus size-5 block"></div>
</BlockHandleAdd>
<BlockHandleDraggable class="flex items-center box-border justify-center h-[1.5em] w-[1.2em] hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-500/50 cursor-grab">
<div class="i-lucide-grip-vertical size-5 block"></div>
</BlockHandleDraggable>
</BlockHandlePopover>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-white dark:bg-gray-950 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, BlockHandlePopover } from 'prosekit/vue/block-handle'
interface Props {
dir?: 'ltr' | 'rtl'
}
const props = defineProps<Props>()
</script>
<template>
<BlockHandlePopover
:placement="props.dir === 'rtl' ? 'right' : 'left'"
class="flex items-center flex-row box-border justify-center transition border-0 [&:not([data-state])]:hidden will-change-transform motion-safe:data-[state=open]:animate-in motion-safe:data-[state=closed]:animate-out motion-safe:data-[state=open]:fade-in-0 motion-safe:data-[state=closed]:fade-out-0 motion-safe:data-[state=open]:zoom-in-95 motion-safe:data-[state=closed]:zoom-out-95 motion-safe:data-[state=open]:animate-duration-150 motion-safe:data-[state=closed]:animate-duration-200"
>
<BlockHandleAdd class="flex items-center box-border justify-center h-[1.5em] w-[1.5em] hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-500/50 cursor-pointer">
<div class="i-lucide-plus size-5 block" />
</BlockHandleAdd>
<BlockHandleDraggable class="flex items-center box-border justify-center h-[1.5em] w-[1.2em] hover:bg-gray-100 dark:hover:bg-gray-800 rounded-sm text-gray-500/50 dark:text-gray-500/50 cursor-grab">
<div class="i-lucide-grip-vertical size-5 block" />
</BlockHandleDraggable>
</BlockHandlePopover>
</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'Styling with Tailwind CSS
Section titled “Styling with Tailwind CSS”ProseKit works well with Tailwind CSS for styling your editor. For pre-built editor components that use Tailwind CSS, see the Components documentation.