Static Renderer
Render saved editor content as styled, read-only HTML.
The static renderer is a small package that displays content produced by
@editorcn/editor or @editorcn/block-editor as read-only, fully styled
HTML — for blog posts, preview panes, emails, or chat message history.
It ships a dedicated stylesheet targeting the exact HTML structure the editors
serialize, so lists, headings, code blocks, tables, task lists, and embeds all
render correctly out of the box. No markdown/typeset.css conversions needed.
The outside is not styled by the stylesheet — the text below is the exact HTML string passed to
StaticRenderer.
Writing on the web
Why prose matters
Your saved document can be rendered anywhere — a blog post, an email, a chat message, a print view. The StaticRenderer styles the exact HTML the editor serializes, so it keeps your typesetting without needing a markdown pipeline.
- Headings and paragraphs
- Lists, nesting, and inline
code - Links like the project site
- Write in the editor
- Save
editor.getHTML() - Render it with
StaticRenderer— read only
-
Nested lists
- … nest just like you would in the editor
Good typography is invisible, and that visible page is all that matters.
Block elements and inline styles pass through untouched, so you can keep
emphasis, italics, underlines, or
struck-through text exactly as the author left it.
Installation
@editorcn/static-renderer can be installed via the shadcn registry
(recommended) or as a regular npm dependency — same as the editors.
- shadcn registry (recommended) copies the component source into your
project (under
components/static-renderer), so you own and can customize it. This is the preferred method since it gives you full control over styling and behavior. - npm installs the package as a regular dependency. Use this if you'd rather not vendor the component source into your repo.
Option 1: shadcn registry (recommended)
Add the editorcn registry to your components.json if you haven't already
(see Getting Started):
{
"$schema": "https://ui.shadcn.com/schema/registry.json",
"registries": {
"@editorcn": "https://editorcn.vercel.app/r/{name}.json"
}
}Then install:
$ pnpm dlx shadcn@latest add @editorcn/static-renderer
The CLI copies StaticRenderer and its stylesheet into
components/static-renderer and installs the required clsx &
tailwind-merge dependencies. Import from the copied source:
import { StaticRenderer } from "@/components/static-renderer";
import "@/components/static-renderer/style.css";Option 2: npm
$ pnpm add @editorcn/static-renderer
Import the component and its stylesheet from the package:
import { StaticRenderer } from "@editorcn/static-renderer";
import "@editorcn/static-renderer/style.css";Order matters: import the stylesheet after your shadcn globals so the CSS variables are defined:
@import "./globals.css"; /* your shadcn theme variables */
@import "@editorcn/static-renderer/style.css";The styles follow your shadcn theme (--foreground, --primary,
--border, --muted, --radius, --font-mono, ...) and adapt to dark mode
automatically.
Component
Import paths below use the npm install. When installed via the shadcn registry, import from
@/components/static-rendererinstead.
import { StaticRenderer } from "@editorcn/static-renderer";
import "@editorcn/static-renderer/style.css";
function PostView({ html }: { html: string }) {
return <StaticRenderer content={html} />;
}It renders a wrapper with the rte-static-renderer class and injects the
content with dangerouslySetInnerHTML. Accepts the usual HTMLAttributes
plus:
| Prop | Type | Default | Description |
|---|---|---|---|
content | string | — | HTML from editor.getHTML() |
as | ElementType | "div" | Root element to render |
className | string | — | Merged with the wrapper class via tailwind-merge |
Without the component
Prefer plain HTML? Add the rte-static-renderer class yourself:
function RenderContent({ html }: { html: string }) {
return (
<div
className="rte-static-renderer"
dangerouslySetInnerHTML={{ __html: html }}
/>
);
}Supported output
The stylesheet covers every block element the editors serialize:
| Content | Selector |
|---|---|
| Headings | .rte-static-renderer h1 – h6 |
| Paragraphs | .rte-static-renderer p |
| Lists | .rte-static-renderer ul, ol, li |
| Blockquote | .rte-static-renderer blockquote |
| Inline code | .rte-static-renderer code |
| Code blocks | .rte-static-renderer pre, pre code |
| Syntax highlighting | .rte-static-renderer pre .hljs-* (lowlight / highlight.js) |
| Horizontal rule | .rte-static-renderer hr |
| Links | .rte-static-renderer a |
| Images | .rte-static-renderer img |
| Tables | .rte-static-renderer table, th, td, .tableWrapper |
| Task lists | .rte-static-renderer ul[data-type="taskList"] |
| Embeds (toolbar) | .rte-static-renderer div[data-type="youtube" / "twitter"] |
Embeds
YouTube and Twitter embeds are serialized as empty <div>s carrying
data-type, data-src / data-tweet-id attributes. The stylesheet renders
them as 16:9 placeholder cards. To show the live media, replace the markup
with an iframe or widget:
import { StaticRenderer } from "@editorcn/static-renderer";
function PostView({ html }: { html: string }) {
return (
<StaticRenderer
content={html.replace(
/<div([^>]*data-type="youtube"[^>]*)><\/div>/g,
"...youtube iframe using the data-src attribute..."
)}
/>
);
}Scoping overrides
Wrap the renderer in a container and scope your overrides:
.my-post {
--radius: 0.25rem;
--primary: oklch(0.7 0.25 150);
}
.my-post .rte-static-renderer p {
line-height: 1.8;
}