Pages CMS

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.json

Usage

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.

PropTypeDefaultDescription
valuestring""Current editor content.
onChange(value: string) => void-Called whenever content changes.
format"markdown" | "html""html"Parsing and output format.
disabledbooleanfalseDisables editing and toolbar actions.
enableImagesbooleantrueEnables image-related behaviors (slash command and image actions).
enableImagePasteDropbooleanfalseEnables 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.
maxImageBytesnumber1000000Max file size used by "data-url" fallback.
onPendingUploadsChange(count: number) => void-Called when pending optimistic uploads change. Use it to gate autosave/save/navigation.
classNamestring-Extra classes for the root wrapper.
editorClassNamestring-Extra classes for the WYSIWYG surface.
...propsHTMLAttributes<HTMLDivElement>-Forwarded to the root container.

Built by Ronan Berder for Pages CMS.