Rich Text
A standalone rich text renderer that accepts a normalized RichTextNode[] tree (typically produced by a CMS mapper) and renders semantic HTML output.
The component uses only React.createElement to walk and render the node tree. No hooks, no state, no use client. It renders complete HTML on the server with zero JavaScript shipped.
Hello, world!
import { RichText } from "@/registry/rokkit200-ui/components/rich-text/rich-text";import { mapCmsType } from "@/registry/rokkit200-ui/lib/content-mapper/content-mapper-util";import type { RichTextCmsField } from "@/registry/rokkit200-ui/lib/rich-text-mapper/rich-text-mapper-types";import { richTextCmsMappers } from "@/registry/rokkit200-ui/lib/rich-text-mapper/rich-text-mapper-util";
export default function RichTextDefaultDemo() { const source = { json: { type: "richText", children: [ { type: "paragraph", children: [ { text: "Hello, world!", bold: true, italic: true, }, ], }, ], }, };
const cmsField = source.json as RichTextCmsField; const content = mapCmsType(richTextCmsMappers, cmsField);
return <RichText content={content} />;}import { RichText } from "@/rokkit200-ui/components/rich-text/rich-text";import { mapCmsType } from "@/rokkit200-ui/lib/content-mapper/content-mapper-util";import type { RichTextCmsField } from "@/rokkit200-ui/lib/rich-text-mapper/rich-text-mapper-types";import { richTextCmsMappers } from "@/rokkit200-ui/lib/rich-text-mapper/rich-text-mapper-util";const source = { json: { type: "richText", children: [ { type: "paragraph", children: [ { text: "Hello, world!", bold: true, italic: true, }, ], }, ], },};
const cmsField = source.json as RichTextCmsField;const content = mapCmsType(richTextCmsMappers, cmsField);
return <RichText content={content} />;Examples
Section titled “Examples”Detailed JSON (via CMS Mapper)
Section titled “Detailed JSON (via CMS Mapper)”Renders structured content mapped from a CMS rich text field (Slate AST). The mapper translates CMS-specific node types (e.g. heading-two, bulleted-list) into normalized RichTextNode[] that the component walks recursively. By default this component applies no styling opinions to the rendered markup.
A Short Guide to Clear Writing
Writing well isn't about using complicated words — it's about communicating ideas clearly.
A good document uses structure, emphasis, and visual elements to guide the reader.
Sometimes that means linking to helpful resources, like the
Plain Language Guidelines.
Why Structure Matters
Large blocks of text are hard to read. Breaking ideas into sections makes content easier to scan.
“Good design is as little design as possible.”
— Dieter Rams
That idea applies to writing too: clarity comes from restraint.
Key Principles
Start with the main idea
Add supporting details
Highlight important phrases
Use subtle emphasis where appropriate
Avoid unnecessary complexity
A Simple Process
Write a rough first draft
Remove unnecessary words
Improve clarity and flow
Add formatting where it helps readers
Inline Code Example
Sometimes you may want to show a command or snippet like:
npm install example-package
Including Images
Images can help break up text and illustrate ideas.
A short caption or explanation often helps readers understand why the image is relevant.
Comparison Table
| Feature | Basic Text | Rich Text |
|---|---|---|
| Formatting | Limited | Flexible |
| Structure | Minimal | Headings, lists, tables |
| Media | None | Images, embeds |
Final Thoughts
Clear communication isn't just about the words themselves — it's about how those words are presented.
If you're curious to learn more about writing for the web, the
Nielsen Norman Group has excellent research on how people read online.
Sometimes a simple divider helps separate ideas:
And that's a quick example of a document that exercises many common rich text rendering features.
import { RichText } from "@/registry/rokkit200-ui/components/rich-text/rich-text";import { mapCmsType } from "@/registry/rokkit200-ui/lib/content-mapper/content-mapper-util";import type { RichTextCmsField } from "@/registry/rokkit200-ui/lib/rich-text-mapper/rich-text-mapper-types";import { richTextCmsMappers } from "@/registry/rokkit200-ui/lib/rich-text-mapper/rich-text-mapper-util";import mockedRichText from "./rich-text-detailed.json";
export default function RichTextDetailedDemo() { const cmsField = mockedRichText.data.MyBlock.item.MainBody .json as unknown as RichTextCmsField; const content = mapCmsType(richTextCmsMappers, cmsField);
return ( <RichText content={content} className="[&_h2]:text-2xl [&_h3]:text-xl [&_h4]:text-lg [&_ol]:list-decimal [&_ol]:pl-6 [&_ul]:list-disc [&_ul]:pl-6" /> );}Custom Component Rendering
Section titled “Custom Component Rendering”Overrides the default rendering for links, images, text marks, and specific element tags using the components prop. This example integrates existing registry components (Link, Image) and applies custom styling to blockquotes, horizontal rules, and table cells.
Each override function receives the typed node and (where applicable) pre-rendered children. Returning undefined from an override falls back to the default rendering for that node, allowing selective customization.
A Short Guide to Clear Writing
Writing well isn't about using complicated words — it's about communicating ideas clearly.
A good document uses structure, emphasis, and visual elements to guide the reader.
Sometimes that means linking to helpful resources, like the
Plain Language Guidelines.
Why Structure Matters
Large blocks of text are hard to read. Breaking ideas into sections makes content easier to scan.
“Good design is as little design as possible.”
— Dieter Rams
That idea applies to writing too: clarity comes from restraint.
Key Principles
Start with the main idea
Add supporting details
Highlight important phrases
Use subtle emphasis where appropriate
Avoid unnecessary complexity
A Simple Process
Write a rough first draft
Remove unnecessary words
Improve clarity and flow
Add formatting where it helps readers
Inline Code Example
Sometimes you may want to show a command or snippet like:
npm install example-package
Including Images
Images can help break up text and illustrate ideas.
A short caption or explanation often helps readers understand why the image is relevant.
Comparison Table
| Feature | Basic Text | Rich Text |
|---|---|---|
| Formatting | Limited | Flexible |
| Structure | Minimal | Headings, lists, tables |
| Media | None | Images, embeds |
Final Thoughts
Clear communication isn't just about the words themselves — it's about how those words are presented.
If you're curious to learn more about writing for the web, the
Nielsen Norman Group has excellent research on how people read online.
Sometimes a simple divider helps separate ideas:
And that's a quick example of a document that exercises many common rich text rendering features.
import { ContentImage } from "@/registry/rokkit200-ui/components/content-image/content-image";import type { PictureProfile } from "@/registry/rokkit200-ui/components/image/image-types";import { Link } from "@/registry/rokkit200-ui/components/link/link";import { RichText } from "@/registry/rokkit200-ui/components/rich-text/rich-text";import { mapCmsType } from "@/registry/rokkit200-ui/lib/content-mapper/content-mapper-util";import type { RichTextCmsField, RichTextComponentMap,} from "@/registry/rokkit200-ui/lib/rich-text-mapper/rich-text-mapper-types";import { richTextCmsMappers } from "@/registry/rokkit200-ui/lib/rich-text-mapper/rich-text-mapper-util";import mockedRichText from "./rich-text-detailed.json";
// Custom component overrides that replace the default rendering for each node type.// This demonstrates how to integrate existing registry components (Link, ContentImage)// and apply custom logic per element tag.const contentImageProfile: PictureProfile = { id: "rich-text-inline", srcSetWidths: [320, 640, 960], sizes: ["(max-width: 768px) 100vw", "(max-width: 1200px) 80vw", "960px"],};
const customComponents: Partial<RichTextComponentMap> = { // Override link rendering to use the registry Link component link: (node, children) => ( <Link href={node.href} target={node.target} rel={node.rel} variant="secondary" className="inline px-2 py-1 text-sm"> {children} </Link> ),
// Override image rendering to use the registry ContentImage component image: node => { const parsedWidth = Number(node.width); const parsedHeight = Number(node.height);
return ( <ContentImage imageSource={{ src: node.url, width: Number.isFinite(parsedWidth) ? parsedWidth : undefined, height: Number.isFinite(parsedHeight) ? parsedHeight : undefined, metadata: { alt: node.alt ?? "" }, }} profile={contentImageProfile} className="my-4 rounded-lg" /> ); },
// Override text rendering to add custom styling based on marks text: node => { if (node.marks?.includes("code")) { return ( <code className="rounded-2xl bg-zinc-100 px-1.5 py-0.5 font-mono text-sm text-slate-900 dark:bg-zinc-800 dark:text-slate-100"> {node.value} </code> ); } // Return undefined for other text nodes to fall back to default rendering return undefined; },
// Override element rendering for specific tags element: (node, children) => { // Custom styled blockquote if (node.tag === "blockquote") { return ( <blockquote className="my-4 border-l-4 border-slate-400 bg-zinc-50 py-2 pl-4 text-slate-900 italic dark:border-slate-500 dark:bg-zinc-900 dark:text-slate-100"> {children} </blockquote> ); }
// Custom styled horizontal rule if (node.tag === "hr") { return ( <hr className="my-5 h-px border-t border-b-0 border-dashed border-slate-400 dark:border-slate-500" /> ); }
// Custom styled table elements if (node.tag === "table") { return ( <div className="my-4 overflow-x-auto rounded-2xl border border-slate-400 dark:border-slate-500"> <table className="w-full">{children}</table> </div> ); }
if (node.tag === "th") { return ( <th className="bg-zinc-600 px-4 py-2 text-left font-semibold text-slate-100 dark:bg-zinc-200 dark:text-slate-900"> {children} </th> ); }
if (node.tag === "td") { return ( <td className="border-t border-slate-400 px-4 py-2 text-slate-900 dark:border-slate-500 dark:text-slate-100"> {children} </td> ); }
if (node.tag === "div") { return <div>{children}</div>; }
if (node.tag === "h1") { return <h1 className="text-5xl font-bold">{children}</h1>; }
if (node.tag === "h2") { return <h2 className="text-4xl font-bold">{children}</h2>; }
if (node.tag === "h3") { return <h3 className="text-2xl font-bold">{children}</h3>; }
if (node.tag === "h4") { return <h4 className="text-xl font-bold">{children}</h4>; }
if (node.tag === "h5") { return <h5 className="text-lg font-bold">{children}</h5>; }
if (node.tag === "h6") { return <h6 className="text-base font-semibold">{children}</h6>; }
// Return undefined for unhandled tags to fall back to default rendering return undefined; },};
export default function RichTextCustomRenderingDemo() { const cmsField = mockedRichText.data.MyBlock.item.MainBody .json as unknown as RichTextCmsField; const content = mapCmsType(richTextCmsMappers, cmsField);
return ( <RichText content={content} className="rounded-2xl border border-slate-400 bg-zinc-100 p-4 text-slate-900 dark:border-slate-500 dark:bg-zinc-800 dark:text-slate-100" components={customComponents} /> );}Capabilities
Section titled “Capabilities”| Capability | Description |
|---|---|
| Structured input | Accepts a normalized RichTextNode[] tree from a CMS mapper |
| Server-side first paint | Uses only React.createElement - no hooks, no state, no use client |
| Semantic rendering | Preserves headings, lists, blockquotes, tables, and links as authored |
| Safe by construction | Output is built from typed node objects, not injected as raw HTML |
| Link safety | External links get safe defaults (noopener noreferrer, configurable) |
| Component overrides | Replace rendering for any node type or specific element tags via components prop |
| Fail safe | Unknown nodes, malformed links, or partial input never crash rendering |
API Reference
Section titled “API Reference”| Property | Type | Required | Description |
|---|---|---|---|
content | RichTextNode[] | undefined | Yes | The normalized rich text node tree |
className | string | No | Applied to the outer wrapper element |
components | Partial<RichTextComponentMap> | No | Node-level render overrides by node type |
linkPolicy | Partial<RichTextLinkPolicy> | No | Override external link safety defaults |
htmlEntityMap | Record<string, string> | No | Override the default HTML entity map for structured text node values, such as |
RichTextNode
Section titled “RichTextNode”A discriminated union of the four renderable node types:
| Type | Description | Key Fields |
|---|---|---|
element | Block or inline HTML element (p, h1, ul, table, etc) | tag, attrs?, children? |
text | Text leaf with optional formatting marks | value, marks? (bold, italic, underline, code) |
link | Anchor wrapping child nodes | href, target?, rel?, isExternal?, className?, children |
image | Self-contained image | url, alt?, width?, height? |
RichTextComponentMap
Section titled “RichTextComponentMap”Override renderers keyed by node type. Each function receives the typed node and (for link and element) pre-rendered children. Return undefined to fall back to the default rendering for that node.
{ text?: (node: RichTextText) => React.ReactNode; image?: (node: RichTextImage) => React.ReactNode; link?: (node: RichTextLink, children?: React.ReactNode) => React.ReactNode; element?: (node: RichTextElement, children?: React.ReactNode) => React.ReactNode;}RichTextLinkPolicy
Section titled “RichTextLinkPolicy”Controls external link behavior. Merged with safe defaults at render time.
| Field | Type | Default | Description |
|---|---|---|---|
externalTarget | string | "_blank" | Target for external links |
externalRel | string | "noopener noreferrer" | Rel attribute for external links |
allowedSchemes | string[] | ["http","https","mailto","tel"] | Permitted URL schemes. Links with disallowed schemes render as <span>. |
Accessibility
Section titled “Accessibility”The RichText component preserves the semantic structure of CMS-authored content. The following guarantees are enforced by design:
- Heading hierarchy is preserved as authored. The component does not remap heading levels (h1-h6).
- List semantics (
ul,ol,li) and table semantics (table,thead,tbody,tr,th,td) are rendered with their correct HTML elements, maintaining screen reader navigation. - Links render as semantic
<a>elements, ensuring they are keyboard reachable and correctly announced by assistive technology. - Meaningful text nodes are never removed or hidden during rendering. Unknown node types are skipped with a dev-mode warning, but their absence does not affect valid content.
- The component is non-interactive by default. No ARIA widget roles are applied. No event handlers are attached.
- Because there is no hydration step (no hooks, no state, no
use client), the rendered HTML is immediately available to assistive technology with no flash of empty content waiting for JavaScript.