@pagehub/editor is a React component. You give it a storage adapter for your files and a canvas host for the things around them (your stylesheet, fonts, a preview link), and it does the rest. It never fetches or hard-codes a URL itself.
Install
React 19 is a peer dependency. Runnable examples (plain HTML, Vite + React, Next.js) are in PageHubApp/demos.
Styles
The editor ships its chrome stylesheet compiled. Import it once:
To compile it in your own Tailwind v4 + DaisyUI build instead, import the source, @pagehub/editor/styles.tailwind.css, from a Tailwind entry file.
Everything is scoped to the editor ([data-ph-editor-scope], which <Editor> puts around its UI and its overlays), and there's no global reset. Embedding it never restyles your page. If your own markup needs Tailwind's global base, import tailwindcss/preflight.css yourself.
Mount it
In React:
Without writing React:
The editor fills its parent, so give the parent a height. While the files load, it shows a skeleton. If the storage can't be opened, it shows an inline error with Retry.
Props
| Prop | What it does |
|---|---|
storage | A storage adapter. <Editor> opens the site from it. |
editor | Instead of storage: an editing core you opened yourself with createEditor. Its extensions must include editorKit(). |
extensions | The kits and plugins for the site. Default: htmlKit(), tailwindKit(), liquidKit(). See extending. |
role | "owner" (default), "editor", "client" (content mode: text and images only, no structure changes) or "viewer" (read-only). |
data | Sample data for data-bound pages in the canvas. |
identity | "session" (default) or "persisted" (data-edit-id stored in the files). |
canvasHost | What the canvas needs from you. See below. |
canvasRender | Replace how the canvas renders a page (for example in a worker). Default: the engine's tags and filters over the editor's data. |
components | Replace or hide any piece of the UI. See below. |
onEvent | Receives saved, conflict, selection, diff and change events. |
onReady | Called with the core once it's open. |
onPainted | Called once the canvas has painted its first page (or failed to), so you can lift your own loading screen. |
autosave | true (or an options object) saves shortly after edits stop and when the person leaves. Off by default: ⌘S and your own save. |
theme | The chrome's color scheme, "light" or "dark". Default: the nearest data-theme around the editor, else the OS setting. |
rightDockOffset | Pixels of your own sidebars on the editor's right edge. See below. |
platform | Pin the shortcut key to "mac" or "other". Detected by default. |
active | false while you hide the editor, so it handles no keys. |
className | Class for the root element. |
The canvas host
The canvas shows the page as visitors see it, so it needs things only you know. Each member is optional, and a member you leave out is simply not done.
| Member | Used for | If left out |
|---|---|---|
siteCss(state) | The site's compiled stylesheet, inlined into every canvas frame. Kept per editor and refreshed in the background. | The page paints with only its own <head>. |
fonts(urls) | Self-hosted copies of the theme's web fonts ({ css, preload }, or null). | The remote font <link> tags. |
runtimeUrl | The site runtime module, booted in each frame in edit mode. | No runtime in the canvas. |
previewUrl(route) | The "Open in new tab" link on the page pill, and the live preview that runs the site's own scripts. | No link. |
pages() | The site's page records (file, route, name), so the page selector shows the routes you serve. | Each page's route comes from its file path. |
host | image_url, file_url, asset_url and icon filter providers. | URLs pass through, and an image id paints a placeholder. |
timezone | The site's IANA timezone, which | date renders in. | UTC. |
data() | Sample or connector data, merged under editor.data. | None. |
devCanvasHost in @pagehub/dev-server/client is a complete implementation over the dev server's endpoints.
Replace parts of the UI
Every visible piece is a named slot. Replace one with your own component, or hide it with null:
The slots are TopBar, PageSelector, BreakpointSwitcher, SaveStatus, Sidebar, AddPanel, LayersPanel, Inspector, InspectorSection, Breadcrumb, Canvas, ComponentBoard, CanvasOverlays, NodeTools, InlineTextToolbar, ContextMenu, CommandPalette, MediaManager, ThemePanel, ModifiersPanel, ScriptsPanel, InspectorDetail, ColorPicker, IconPicker, FontPicker, CodeView, DiffPreview, SaveBanner and EmptyState. The props each receives are exported as types (SlotProps, MediaManagerProps, PanelProps and so on).
@pagehub/editor/componentsexports the default components, so you can wrap one instead of rewriting it.@pagehub/editor/primitivesexports the building blocks for new panels:Modal,Drawer,FloatingPanel,AnchoredPopover,Chip,PopoverChip, the toolbar controls,MenuSurface,Skeleton,Spinner,InlineError,Iconandtooltip.- Inside a slot,
useEditorStore,useEditorState,useRunCommandanduseUiread and drive the editor.
Media is yours to provide. MediaManager is null by default, so the Media panel and the image picker stay inert until you pass one. The editor exports the standard manager from @pagehub/editor/components:
The manager reads its library from the mediaFacet in @pagehub/engine/host. Other catalogues you can supply there: icons, fonts, theme colors, blocks, theme presets and font pairings.
The right-hand drawer
The big panels (Theme, Media, Modifiers, Scripts) and the inspector's larger editors (Show only when, Repeat for each, Edit HTML, Action, Event handlers) open in one drawer docked to the editor's right edge, one at a time. It pushes the canvas aside instead of covering it, and Esc closes it.
If your app has sidebars on the same edge, tell the editor how wide they are:
The editor reserves that many pixels on its right as padding, so the canvas never runs under your sidebars and the drawer opens just inboard of them. The default is 0. Change the number whenever your sidebars open, close or resize.
Save errors
When the storage throws a StorageError, the save banner and the top bar's save icon say what happened, using the error's code:
maintenanceis editing paused, not a failure. It shows an amber status (with the storage'sdetailtext if it gave one), keeps the draft in the editor and offers Try again. It clears itself on the next successful save.networksays the server can't be reached, with Try again.format_changedsays the site was upgraded and the page must be reloaded.forbidden,not-found,too-largeandconflicteach say what happened, with no Try again. Any other failure is a generic message with Try again.
If your storage has writeBatch, a save is one call, so a conflict on any file saves none of them. The conflict banner (Reload theirs / Keep mine) lists the stale files.
Events
type | Fields | When |
|---|---|---|
change | dirty | The text changed and went quiet. dirty is what a save would write. |
saved | written | A save wrote files. |
conflict | path, theirs, version | A file collided with a newer version. |
selection | handles, primary | The selection changed. |
diff | files | Every change, with the pending diff per file. |