/
209
Sponsor

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.

Static read-only output

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
  1. Write in the editor
  2. Save editor.getHTML()
  3. 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.

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-renderer instead.

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:

PropTypeDefaultDescription
contentstringHTML from editor.getHTML()
asElementType"div"Root element to render
classNamestringMerged 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:

ContentSelector
Headings.rte-static-renderer h1h6
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;
}