Sanitized HTML
Renders HTML you did not write — rich-text fields, imported descriptions, API copy — through DOMPurify with a conservative tag and attribute allowlist.
import { SanitizedHtml } from '@/components/custom/sanitized-html'
// Two attacks and some legitimate markup. The script tag and the onerror
// handler are stripped; the bold text and the link survive.
const UNTRUSTED = `
<p>Approved by <strong>Ada Lovelace</strong> on 12 Aug.</p>
<p>See the <a href="https://ui.kiotapay.com" target="_blank">policy</a> for details.</p>
<script>alert('xss')</script>
<img src="x" onerror="alert('xss')" />
`
export default function SanitizedHtmlDemo() {
return (
<div className="w-full max-w-xl space-y-4">
<div>
<p className="mb-2 text-sm font-medium text-muted-foreground">Untrusted input</p>
<pre className="overflow-x-auto rounded-md border bg-muted/40 p-3 text-xs">{UNTRUSTED.trim()}</pre>
</div>
<div>
<p className="mb-2 text-sm font-medium text-muted-foreground">Rendered output</p>
<SanitizedHtml dirtyHtml={UNTRUSTED} className="rounded-md border p-3 text-sm" />
</div>
</div>
)
}Installation
pnpm dlx shadcn@latest add https://kiotapay-ui-staging.staging.kiotapay.com/r/sanitized-html.jsonnpx shadcn@latest add https://kiotapay-ui-staging.staging.kiotapay.com/r/sanitized-html.jsonyarn dlx shadcn@latest add https://kiotapay-ui-staging.staging.kiotapay.com/r/sanitized-html.jsonbunx --bun shadcn@latest add https://kiotapay-ui-staging.staging.kiotapay.com/r/sanitized-html.jsonAdds dompurify to your dependencies.
Usage
import { SanitizedHtml } from '@/components/custom/sanitized-html'
export function PolicyBody({ policy }) {
return <SanitizedHtml dirtyHtml={policy.body} className="prose text-sm" />
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
dirtyHtml | string | — | Required. Untrusted markup. |
className | string | '' | Merged with sanitized-html-content. |
allowedTags | string[] | see below | Tags that survive sanitising. |
allowedAttributes | string[] | ['href', 'title', 'target', 'rel', 'class'] | Attributes that survive. |
The default tag list is inline formatting, links, paragraphs, lists, headings, span and div. No img, no script, no style, no event handlers.
Widen allowedTags deliberately. Every tag added is one an attacker can reach for, and the list is the whole of the
defence — dangerouslySetInnerHTML does exactly what it says once something gets past it.
Behaviour notes
The first paint is empty. Sanitising needs a real DOM, so it runs in an effect: doing it during a server render would throw or require jsdom, and doing it inline on the client only would make the server and client markup disagree and fail hydration. The cleaned markup lands immediately after mount.
Pass stable arrays. allowedTags and allowedAttributes are effect dependencies, so a fresh array literal on every render re-sanitises every render. Hoist them to module scope, or leave them at their defaults.