The RichText component is a React component for rendering Optimizely CMS rich text content. It transforms structured JSON content from the CMS into React elements with full customization support.
Tip
Using the <RichText/> component is the recommended way to render rich text content. It's safer than dangerouslySetInnerHTML as it doesn't rely on HTML parsing, and allows you to customize how elements are rendered with your own React components.
import { RichText } from '@optimizely/cms-sdk/react/richText';import { RichText } from '@optimizely/cms-sdk/react/richText';
function Article({ content }) {
return (
<div>
<RichText content={content.body?.json} />
</div>
);
}[!IMPORTANT] Integration API Limitations
When rich text content is created through Optimizely's Integration API (REST API) rather than the CMS editor interface, certain features may not function correctly:
- Inline styles - Some inline CSS properties might not work as expected. See Attribute and CSS Property Support for details on supported CSS properties.
- HTML validation is bypassed, which can result in malformed or invalid HTML that causes rendering issues.
- Advanced formatting that relies on TinyMCE editor processing may be missing.
- Custom attributes or props might not be mapped properly from raw HTML to React components.
- Security sanitization performed by the editor may not occur.
For full feature compatibility and optimal rendering, create rich text content using the CMS's built-in TinyMCE editor interface.
Type: { type: 'richText', children: Node[] } | null | undefined
The rich text content from Optimizely CMS. This is typically accessed from a content type property with richText type.
<RichText content={article.body?.json} />Type: Record<string, React.ComponentType<ElementProps>>
Default: Built-in HTML element mappings
Custom React components for rendering specific element types. Allows you to override how different block and inline elements are rendered.
All element types listed below are supported by default and can be customized with the elements prop:
- Headings:
heading-one,heading-two,heading-three,heading-four,heading-five,heading-six - Text blocks:
paragraph,quote,div - Lists:
bulleted-list,numbered-list,list-item - Text semantics (inline):
span,mark,strong,em,u,s,i,b,small,sub,sup,ins,del,kbd,abbr,cite,dfn,q,data,bdo,bdi - Code-related:
code,pre,var,samp - Links & Interactive:
link,a,button,label - Tables:
table,thead,tbody,tfoot,caption,tr,th,td
[!NOTE] > SVG elements are not supported by default. SVG requires specialized child elements (
circle,path,rect, etc.) and attributes that would require extensive additional support.Alternatives: Use custom element handlers, upload SVG as image assets, or create dedicated React components.
import { RichText, ElementProps } from '@optimizely/cms-sdk/react/richText';
// Custom heading with styling
const CustomHeading = ({ children, element }: ElementProps) => (
<h1 className="text-4xl font-bold text-blue-600 mb-4">{children}</h1>
);
// Custom link with tracking
const CustomLink = ({ children, element }: ElementProps) => {
const linkElement = element as LinkElement;
return (
<a
href={linkElement.url}
className="text-blue-500 hover:underline"
onClick={() => trackLinkClick(linkElement.url)}
target={linkElement.target}
rel={linkElement.rel}
>
{children}
</a>
);
};
// Custom quote block
const CustomQuote = ({ children }: ElementProps) => (
<blockquote className="border-l-4 border-gray-300 pl-4 py-2 italic text-gray-600">
{children}
</blockquote>
);
function Article({ content }) {
return (
<RichText
content={content.body?.json}
elements={{
'heading-one': CustomHeading,
link: CustomLink,
quote: CustomQuote,
}}
/>
);
}Type: Record<string, React.ComponentType<LeafProps>>
Default: Built-in text formatting mappings
Custom React components for rendering text formatting marks (bold, italic, etc.).
bold- Bold text formattingitalic- Italic text formattingunderline- Underlined textstrikethrough- Strikethrough textcode- Inline code formatting
import { RichText, LeafProps } from '@optimizely/cms-sdk/react/richText';
// Custom bold with additional styling
const CustomBold = ({ children }: LeafProps) => (
<strong className="font-extrabold text-gray-900">{children}</strong>
);
// Custom code with syntax highlighting theme
const CustomCode = ({ children }: LeafProps) => (
<code className="bg-gray-100 px-1 py-0.5 rounded font-mono text-sm text-red-600">
{children}
</code>
);
// Custom highlight leaf (for custom mark)
const CustomHighlight = ({ children }: LeafProps) => (
<mark className="bg-yellow-200 px-1 rounded">{children}</mark>
);
function Article({ content }) {
return (
<RichText
content={content.body?.json}
leafs={{
bold: CustomBold,
code: CustomCode,
highlight: CustomHighlight, // Custom mark
}}
/>
);
}Type: boolean
Default: true
Controls whether HTML entities in text content should be decoded. When enabled, entities like <, >, & are converted to their corresponding characters.
// Default behavior - entities are decoded
<RichText
content={content.body?.json}
decodeHtmlEntities={true} // <div> becomes <div>
/>
// Preserve HTML entities as-is
<RichText
content={content.body?.json}
decodeHtmlEntities={false} // <div> stays as <div>
/>// When displaying code examples where entities should remain encoded
const CodeExample = ({ content }) => (
<div className="code-display">
<RichText
content={content}
decodeHtmlEntities={false}
elements={{
paragraph: ({ children }) => <pre>{children}</pre>,
}}
/>
</div>
);import React from 'react';
import {
RichText,
ElementProps,
LeafProps,
} from '@optimizely/cms-sdk/react/richText';
// Custom element components
const CustomHeading = ({ children, element }: ElementProps) => (
<h1 className="text-3xl font-bold mb-4 text-slate-800">{children}</h1>
);
const CustomParagraph = ({ children }: ElementProps) => (
<p className="mb-4 text-slate-600 leading-relaxed">{children}</p>
);
const CustomLink = ({ children, element }: ElementProps) => {
const link = element as { url: string; target?: string };
return (
<a
href={link.url}
target={link.target}
className="text-blue-600 hover:text-blue-800 underline"
>
{children}
</a>
);
};
// Custom leaf components
const CustomBold = ({ children }: LeafProps) => (
<strong className="font-semibold text-slate-900">{children}</strong>
);
const CustomItalic = ({ children }: LeafProps) => (
<em className="italic text-slate-700">{children}</em>
);
const CustomCode = ({ children }: LeafProps) => (
<code className="bg-slate-100 px-2 py-1 rounded text-sm font-mono text-slate-800">
{children}
</code>
);
export default function Article({ content }) {
return (
<article className="prose max-w-none">
<RichText
content={content.body?.json}
elements={{
'heading-one': CustomHeading,
paragraph: CustomParagraph,
link: CustomLink,
}}
leafs={{
bold: CustomBold,
italic: CustomItalic,
code: CustomCode,
}}
decodeHtmlEntities={true}
/>
</article>
);
}The component is fully typed with TypeScript. Import the prop types for better development experience:
import {
RichText,
RichTextProps,
ElementProps,
LeafProps,
ElementMap,
LeafMap,
} from '@optimizely/cms-sdk/react/richText';
// Type-safe element mapping
const elements: ElementMap = {
'heading-one': CustomHeading,
paragraph: CustomParagraph,
};
// Type-safe leaf mapping
const leafs: LeafMap = {
bold: CustomBold,
italic: CustomItalic,
};The RichText component uses a simple and safe fallback strategy for unknown elements and text marks.
All unknown elements and text marks render as <span> elements.
This ensures:
- Valid HTML in any context (inline or block)
- No hydration errors in React
- Safe rendering without breaking layout
- Can be styled via CSS if needed
- Known Elements: Use their default HTML tags (e.g.,
heading-one→<h1>,paragraph→<p>) - Unknown Elements: Automatically become
<span>tags - Known Text Marks: Use their default tags (e.g.,
bold→<strong>,italic→<em>) - Unknown Text Marks: Automatically become
<span>tags
If you encounter unknown HTML tags or elements not supported by the SDK, you can override them using the elements or leafs props to render custom React components.
Note
Unknown elements and text marks are typically introduced when rich text content is created through the Integration API (REST API) rather than the CMS editor. When using the Integration API, developers can insert custom HTML elements or formatting that may not be recognized by the SDK's default element mappings. Additionally, some element attributes may not be fully supported when content is created via the Integration API, as the TinyMCE editor processing that normalizes and validates these attributes is bypassed.
import {
RichText,
ElementProps,
LeafProps,
} from '@optimizely/cms-sdk/react/richText';
// Custom component for unknown block elements
const UnknownElement = ({ children, element }: ElementProps) => (
<div className="unknown-element" data-type={element.type}>
{children}
</div>
);
// Custom component for unknown text marks
const UnknownLeaf = ({ children, leaf }: LeafProps) => (
<span className="unknown-leaf" data-marks={Object.keys(leaf).join(',')}>
{children}
</span>
);
// Custom component for a specific custom element
const CustomElement = ({ children, element }: ElementProps) => (
<div className="custom-element">{children}</div>
);
function Article({ content }) {
return (
<RichText
content={content.body?.json}
elements={{
'unknown-element': UnknownElement, // Handle specific unknown element type
'custom-element': CustomElement, // Your custom CMS element
}}
leafs={{
'unknown-leaf': UnknownLeaf, // Handle specific unknown text mark
highlight: ({ children }) => (
<mark className="highlight">{children}</mark>
),
}}
/>
);
}// Custom video element not supported by default
const VideoElement = ({ children, element }: ElementProps) => {
const videoData = element as { videoUrl?: string; autoplay?: boolean };
return (
<div className="video-container">
<video src={videoData.videoUrl} autoPlay={videoData.autoplay} controls>
{children}
</video>
</div>
);
};
// Custom callout box element
const CalloutElement = ({ children, element }: ElementProps) => {
const calloutData = element as { variant?: string };
return (
<div className={`callout callout-${calloutData.variant || 'info'}`}>
{children}
</div>
);
};
function Article({ content }) {
return (
<RichText
content={content.body?.json}
elements={{
'video-embed': VideoElement,
'callout-box': CalloutElement,
}}
/>
);
}Under the hood, the RichText component performs sophisticated attribute and CSS property processing to ensure React compatibility. Here's how it handles the technical challenges of converting CMS content to proper React elements.
🔧 Technical Deep Dive: The component normalizes HTML attributes to camelCase React props and moves CSS properties to React's
styleobject, preventing warnings and ensuring proper rendering.
The component automatically normalizes HTML attributes to React-compatible prop names. This conversion happens through a predefined mapping table that handles common naming conflicts between HTML and React.
⚠️ React Compatibility: React requires camelCase prop names for DOM attributes, but HTML uses kebab-case. Our mapping system handles this conversion automatically to prevent console warnings.
Reserved Keywords (Must be mapped):
class→classNamefor→htmlFor
Table Attributes:
colspan→colSpanrowspan→rowSpancellpadding→cellPaddingcellspacing→cellSpacing
tabindex,tab-index→tabIndexreadonly→readOnlymaxlength→maxLengthminlength→minLengthautocomplete→autoCompleteautofocus→autoFocusautoplay→autoPlaycontenteditable,content-editable→contentEditablespellcheck→spellChecknovalidate→noValidate
crossorigin→crossOriginusemap→useMapallowfullscreen→allowFullScreenframeborder→frameBorderplaysinline→playsInlinesrcset→srcSetsrcdoc→srcDocsrclang→srcLang
accept-charset→acceptCharsethttp-equiv→httpEquivcharset→charSetdatetime→dateTimehreflang→hrefLangaccesskey→accessKeyautocapitalize→autoCapitalizereferrerpolicy→referrerPolicyformaction→formActionformenctype→formEnctypeformmethod→formMethodformnovalidate→formNoValidateformtarget→formTargetenctype→encType
🚀 Performance Optimization: Many HTML attributes work directly in React without conversion, so they're not included in our mapping table for better performance.
These attributes work directly in React:
- Basic:
id,name,value,type,href,src,alt,title - Form:
disabled,checked,selected,multiple,required,placeholder,pattern,min,max,step - Interactive:
draggable,hidden,lang,dir,role - Media:
width,height,preload,loop,muted,controls - Security:
nonce,sandbox,download
ARIA Attributes (Preserved as-is): All ARIA attributes remain in kebab-case as React requires:
aria-label,aria-labelledby,aria-describedbyaria-hidden,aria-expanded,aria-*(all ARIA attributes)
Data Attributes (Preserved as-is):
data-*(all data attributes are preserved in kebab-case)
CSS properties are automatically detected using a curated set of known CSS property names and moved to React's style object with proper camelCase conversion.
🎨 Style Processing: React requires CSS properties to be in a style object with camelCase keys. Properties like
background-colorbecomestyle.backgroundColor. This system prevents invalid DOM property warnings.
min-width→style.minWidthmax-width→style.maxWidthmin-height→style.minHeightmax-height→style.maxHeight
margin,margin-top,margin-right,margin-bottom,margin-leftpadding,padding-top,padding-right,padding-bottom,padding-left
font,font-family,font-size,font-weight,font-style,font-variantline-height,letter-spacing,word-spacingtext-align,text-decoration,text-transform,text-indent,text-shadowvertical-align
colorbackground,background-color,background-image,background-repeatbackground-position,background-size,background-attachmentbackground-clip,background-origin
border-width,border-style,border-color,border-radiusborder-top,border-right,border-bottom,border-leftborder-*-width,border-*-style,border-*-color(all directional variants)border-*-radius(all corner variants)
position,top,right,bottom,left,z-indexfloat,clear
display,visibility,opacityoverflow,overflow-x,overflow-yclip,clip-path
flex,flex-direction,flex-wrap,flex-flowjustify-content,align-items,align-content,align-selfflex-grow,flex-shrink,flex-basis
grid,grid-template,grid-template-rows,grid-template-columnsgrid-template-areas,grid-area,grid-row,grid-columngrid-gap,gap,row-gap,column-gap
white-space,word-wrap,word-break,overflow-wraphyphens,text-overflow,direction,unicode-bidi,writing-mode
box-shadow,text-shadowfilter,backdrop-filtertransform,transform-originperspective,perspective-origin
transition,transition-property,transition-durationtransition-timing-function,transition-delayanimation,animation-name,animation-durationanimation-timing-function,animation-delayanimation-iteration-count,animation-directionanimation-fill-mode,animation-play-state
cursor,pointer-events,user-select,resize
aspect-ratio,object-fit,object-positionscroll-behavior,overscroll-behaviorscroll-snap-type,scroll-snap-alignscroll-margin,scroll-padding
Some properties exist in both HTML and CSS domains. The component uses context-aware logic to determine the correct handling:
width,height: Treated as HTML attributes for tables and images, CSS properties for other elementsborder: Treated as HTML attribute for tables, CSS property for other elements
Table elements receive special handling for HTML attributes:
// These remain as HTML attributes for tables
<table border="1" cellpadding="5" cellspacing="0" width="100%">
<tr>
<td colspan="2" rowspan="1">
Content
</td>
</tr>
</table>The component performs real-time CSS property conversion using kebab-to-camelCase transformation:
// Input: Slate.js node with CSS properties as attributes
{
type: 'div',
'font-size': '16px',
'background-color': 'blue',
'margin-top': '10px',
children: [{ text: 'Styled content' }]
}
// Output: React element with style object
<div style={{
fontSize: '16px',
backgroundColor: 'blue',
marginTop: '10px'
}}>
Styled content
</div>While our CSS property detection covers most real-world use cases, some advanced CSS features are intentionally excluded to maintain performance and simplicity:
page-break-before,page-break-after,page-break-insideorphans,widows
- Multi-column layout properties (
columns,column-count, etc.) - CSS Masking properties (
mask,mask-image, etc.) - Ruby text properties (
ruby-align,ruby-position, etc.)
margin-inline-start,margin-block-end, etc.border-inline-start,border-block-end, etc.
- CSS variables (
--custom-property) are not automatically detected
If you encounter unsupported properties in your CMS content, you can handle them by providing custom element components:
🔧 Extensibility: The component is designed to be extended rather than modified. Use custom element components for application-specific handling of unsupported properties.
// Custom handling for unsupported properties
const CustomDiv = ({ children, element, attributes }: ElementProps) => {
const customStyle = {
// Handle unsupported CSS properties manually
'--custom-property': attributes['--custom-property'],
columnCount: attributes['column-count'],
};
return <div style={customStyle}>{children}</div>;
};
<RichText
content={content}
elements={{
div: CustomDiv,
}}
/>;Here's how the attribute processing works in practice with a real Slate.js node structure:
// Rich text content in Slate.js format with mixed attributes
const richTextContent = {
type: 'richText',
children: [
{
type: 'div',
class: 'content-block',
'data-testid': 'rich-content',
'aria-label': 'Article content',
width: '100%', // HTML attribute for some elements
'font-size': '16px', // CSS property → style.fontSize
'background-color': 'lightblue', // CSS property → style.backgroundColor
'margin-top': '20px', // CSS property → style.marginTop
border: '1px solid #ccc', // CSS property → style.border
children: [
{ text: 'This content has mixed HTML attributes and CSS properties' },
],
},
],
};
// Rendered as:
<div
className="content-block"
data-testid="rich-content"
aria-label="Article content"
width="100%"
style={{
fontSize: '16px',
backgroundColor: 'lightblue',
marginTop: '20px',
border: '1px solid #ccc',
}}
>
This content has mixed HTML attributes and CSS properties
</div>;This attribute and CSS property processing ensures that rich text content from Optimizely CMS renders correctly in React applications while maintaining proper HTML semantics and React compatibility.
- Performance: Only override elements/leafs you need to customize - the default implementations are optimized
- Accessibility: Maintain semantic HTML structure in custom components - screen readers depend on proper markup
- Type Safety: Use TypeScript interfaces for better development experience and catch errors at compile time
- Unknown Elements: The default
<span>fallback handles unknown elements safely - no configuration needed
<RichText
content={post.content?.json}
elements={{
'heading-one': ({ children }) => (
<h1 className="article-title">{children}</h1>
),
paragraph: ({ children }) => (
<p className="article-paragraph">{children}</p>
),
}}
/><RichText
content={doc.body?.json}
elements={{
code: ({ children }) => <CodeBlock>{children}</CodeBlock>,
pre: ({ children }) => <PreformattedText>{children}</PreformattedText>,
}}
decodeHtmlEntities={false} // Preserve code entities
/><RichText
content={page.content?.json}
elements={{
link: ({ children, element }) => (
<TrackableLink url={element.url} eventName="content-click">
{children}
</TrackableLink>
),
}}
/>