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. You can also pass onRequestImage to replace the default image prompt with your own modal or picker, plus onUploadImage for paste/drop uploads.
During uploads, the editor inserts a temporary local preview immediately, then preloads the final uploaded URL before swapping src. If preload fails or times out, it keeps the temporary preview and marks an upload error on that image.
import { useState } from "react"
import { Editor } from "@/components/ui/editor"
export function Example() {
const [value, setValue] = useState("")
return (
<Editor
value={value}
onChange={setValue}
onRequestImage={async () => {
const src = window.prompt("Use URL? Leave empty to simulate file upload")
if (src) return { kind: "url", src, alt: "Optional alt text" }
return { kind: "file", file: new File(["demo"], "demo.png", { type: "image/png" }) }
}}
enableImagePasteDrop
imageFallback="data-url"
onUploadImage={async (file) => ({
src: await fileToDataUrl(file),
alt: file.name,
})}
/>
)
}The default format is "html". Set format="markdown" if your application stores Markdown.
Implement Your Own Source Toggle
Keep one shared value state and switch between Editor and your own source input. This keeps source-mode logic in your app, while the component stays focused on rich-text editing.
import { useState } from "react"
import { Editor } from "@/components/ui/editor"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Textarea } from "@/components/ui/textarea"
type View = "editor" | "source"
export function EditorWithSourceToggle() {
const [view, setView] = useState<View>("editor")
const [value, setValue] = useState("")
return (
<Tabs value={view} onValueChange={(next) => setView(next as View)} className="w-full">
<TabsList>
<TabsTrigger value="editor">Editor</TabsTrigger>
<TabsTrigger value="source">Source</TabsTrigger>
</TabsList>
<TabsContent value="editor">
<Editor value={value} onChange={setValue} format="markdown" />
</TabsContent>
<TabsContent value="source">
<Textarea
value={value}
onChange={(event) => setValue(event.target.value)}
className="min-h-64 font-mono"
/>
</TabsContent>
</Tabs>
)
}Typography
The registry component ships with shadcn/ui Typeset styling for editor content. No separate typography stylesheet is required.
Options
Editor is a controlled component. The props below define content format and presentation 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. |
disabled | boolean | false | Disables editing and toolbar actions. |
enableImages | boolean | true | Enables image-related behaviors (slash command and image actions). |
enableImagePasteDrop | boolean | false | Enables image file paste and drag/drop insertion. |
onRequestImage | (ctx) => { kind: "url", src, alt?, title? } | { kind: "file", file, alt?, title? } | null | Promise<{ kind: "url", src, alt?, title? } | { kind: "file", file, alt?, title? } | null> | - | Called when the user picks the Image slash command. Return image data to insert it; return null to cancel. If omitted, the editor falls back to a native URL prompt. |
onUploadImage | (file, ctx) => { src, alt?, title? } | null | Promise<{ src, alt?, title? } | null> | - | Called for pasted or dropped image files. Return image data to insert. If missing or returning null, fallback behavior is used. |
imageFallback | "data-url" | "prompt-url" | "none" | "prompt-url" | Fallback strategy when no image callback inserts content. For paste/drop, only "data-url" can insert from files. |
maxImageBytes | number | 1000000 | Max file size used by "data-url" fallback. |
onPendingUploadsChange | (count: number) => void | - | Called when pending optimistic uploads change. Use it to gate autosave/save/navigation. |
className | string | - | Extra classes for the root wrapper. |
editorClassName | string | - | Extra classes for the WYSIWYG surface. |
...props | HTMLAttributes<HTMLDivElement> | - | Forwarded to the root container. |
Built by Ronan Berder for Pages CMS.