If you're using Directus as a headless CMS, the built-in WYSIWYG editor outputs raw HTML. The challenge is rendering it in React safely, with proper styling, and without reaching for dangerouslySetInnerHTML.
Here's how to solve it with three packages working together.
The Stack
- DOMPurify — sanitizes HTML, stripping XSS vectors before they touch the DOM
- html-react-parser — converts sanitized HTML into React elements
-
Tailwind CSS v4 Typography — styles everything automatically with the
proseclass
Installing the Packages
npm install dompurify html-react-parser
npm install @tailwindcss/typography
Step 1 — Sanitize the HTML
Create a reusable utility function that runs DOMPurify on any HTML string coming from Directus. The typeof window === 'undefined' guard handles SSR — DOMPurify is browser-only.
export const sanitizeHtml = (html: string) => {
if (typeof window === 'undefined') return html;
return DOMPurify.sanitize(html);
};
Step 2 — Parse and Render
Pass the sanitized HTML through html-react-parser to convert it into React elements. Wrap it in a prose div and Tailwind Typography handles all the styling.
import parse from 'html-react-parser';
import { sanitizeHtml } from '@/utils/sanitizeHtml';
<div className="prose dark:prose-invert prose-ul:text-foreground
prose-li:marker:text-foreground prose-strong:text-foreground max-w-none">
{parse(sanitizeHtml(product.description))}
</div>
What You Get
- ✅ XSS safe — DOMPurify strips malicious scripts before rendering
- ✅ No
dangerouslySetInnerHTML— html-react-parser converts to React elements directly - ✅ Dark mode ready —
prose-inverthandles the color flip automatically - ✅ Design system aware —
prose-strong:text-foregroundandprose-ul:text-foregroundpull from your CSS variables so text always matches your theme - ✅ Headings, lists, bold, spacing — all styled automatically by Tailwind Typography
Why Not Just Use dangerouslySetInnerHTML?
dangerouslySetInnerHTML renders raw HTML with no sanitization. If your CMS content ever contains injected scripts — whether from a compromised editor, a bad import, or a malicious user — it executes directly in the browser. DOMPurify + html-react-parser gives you the same rendered output with none of the risk.
This pattern works with any headless CMS that outputs HTML from a rich text editor — Directus, Payload, Strapi, or Contentful. One utility function and a prose wrapper is all it takes.
Top comments (0)