Pages Editor
A simple, Notion-like WYSIWYG editor component for shadcn/ui. Built with TipTap, ProseMirror, and React.
Demo
Documentation
Install
Install this specific component directly from its JSON URL.
npx shadcn@latest add https://editor.pagescms.org/r/editor.jsonUsage
Use controlled state and pass format="markdown" or format="html" depending on how you store content.
import { useState } from "react"
import { Editor } from "@/components/ui/editor"
export function Example() {
const [value, setValue] = useState("")
return (
<Editor
value={value}
onChange={setValue}
/>
)
}The default format is "html". Set format="markdown" if your application stores Markdown.
Optional Mode Switch
If you want source mode, control mode from your own UI.
import { useState } from "react"
import { Editor } from "@/components/ui/editor"
export function Example() {
const [mode, setMode] = useState<"wysiwyg" | "source">("wysiwyg")
const [value, setValue] = useState("")
return (
<>
<button type="button" onClick={() => setMode("wysiwyg")}>Editor</button>
<button type="button" onClick={() => setMode("source")}>Source</button>
<Editor
value={value}
onChange={setValue}
format="markdown"
mode={mode}
/>
</>
)
}Default Content Styles
The editor works without these styles, but you can copy this baseline stylesheet to get the same content typography as the demo.
.cn-editor .tiptap > :first-child {
margin-top: 0;
}
.cn-editor .tiptap > :last-child {
margin-bottom: 0;
}
.cn-editor .tiptap h1 {
margin: 2rem 0 0.75rem;
font-size: 2.25rem;
font-weight: 800;
line-height: 1.2;
letter-spacing: -0.02em;
}
.cn-editor .tiptap h2 {
margin: 2rem 0 0.75rem;
border-bottom: 1px solid hsl(var(--border));
padding-bottom: 0.5rem;
font-size: 1.875rem;
font-weight: 600;
line-height: 1.3;
}
.cn-editor .tiptap h3 {
margin: 1.5rem 0 0.5rem;
font-size: 1.5rem;
font-weight: 600;
line-height: 1.35;
}
.cn-editor .tiptap p {
line-height: 1.75;
}
.cn-editor .tiptap p + p {
margin-top: 1rem;
}
.cn-editor .tiptap ul,
.cn-editor .tiptap ol {
margin: 1rem 0;
margin-left: 1.5rem;
}
.cn-editor .tiptap ul {
list-style: disc;
}
.cn-editor .tiptap ol {
list-style: decimal;
}
.cn-editor .tiptap blockquote {
margin-top: 1.5rem;
border-left: 2px solid hsl(var(--border));
padding-left: 1.5rem;
font-style: italic;
}
.cn-editor .tiptap a {
color: hsl(var(--primary));
font-weight: 500;
text-decoration-line: underline;
text-decoration-style: dotted;
text-underline-offset: 4px;
}
.cn-editor .tiptap code {
border-radius: calc(var(--radius) - 4px);
background: hsl(var(--muted));
padding: 0.2rem 0.3rem;
font-family: var(--font-mono);
font-size: 0.875rem;
}
.cn-editor .tiptap pre {
margin: 1rem 0;
overflow-x: auto;
border-radius: var(--radius);
background: rgb(9 9 11 / 0.95);
padding: 1rem;
}
.cn-editor .tiptap pre code {
background: transparent;
padding: 0;
color: rgb(250 250 250);
}
.cn-editor .tiptap table {
margin: 1rem 0;
width: 100%;
border-collapse: collapse;
}
.cn-editor .tiptap th,
.cn-editor .tiptap td {
border: 1px solid hsl(var(--border));
padding: 0.5rem 0.75rem;
text-align: left;
}
.cn-editor .tiptap th {
background: hsl(var(--muted));
font-weight: 500;
}
Options
Editor is a controlled component. The props below define content format, mode, and source synchronization behavior.
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | "" | Current editor content. |
onChange | (value: string) => void | - | Called whenever content changes. |
format | "markdown" | "html" | "html" | Parsing and output format. |
mode | "wysiwyg" | "source" | "wysiwyg" | Active editor mode. |
disabled | boolean | false | Disables editing and toolbar actions. |
sourceDebounceMs | number | 500 | Debounce for source text sync. |
className | string | - | Extra classes for the root wrapper. |
editorClassName | string | - | Extra classes for the WYSIWYG surface. |
sourceClassName | string | - | Extra classes for the source textarea. |
...props | HTMLAttributes<HTMLDivElement> | - | Forwarded to the root container. |
Hooks
Use these hooks when you need to transform content during mode transitions.
| Hook | Type | When it runs | Return |
|---|---|---|---|
onSwitchToSource | (value: string, format: "markdown" | "html") => string | Promise<string> | When switching from WYSIWYG to source mode. | Transformed source content. |
onSwitchToEditor | (value: string, format: "markdown" | "html") => string | Promise<string> | When switching from source mode back to WYSIWYG. | Transformed editor content. |
Built by Ronan Berder for Pages CMS.