Getting started
Install
Section titled “Install”npm install @johnhenry/safe-fragment<!-- In <head>, before any <script type="module"> or modulepreload. --><script type="importmap"> { "imports": { "@johnhenry/safe-fragment": "https://cdn.jsdelivr.net/npm/@johnhenry/[email protected]/dist/index.js", "dompurify": "https://cdn.jsdelivr.net/npm/[email protected]/dist/purify.es.mjs" } }</script><script type="module"> import * as safeFragment from "@johnhenry/safe-fragment";</script>Provenance. This is a new package: never published under any other name, and 0.0.0 is the unreleased development
version, so there is no earlier name or version to migrate from.
dompurify (pinned to an exact version) is a normal dependency, installed automatically. It is loaded lazily, only on browsers
without Element.setHTML (Safari today) or when you force the fallback. The shipped ESM and CJS output targets evergreen
browsers; Node 26 or newer is only the toolchain (devEngines).
From git. Until there is a release, a commit can be installed directly:
npm install git+https://github.com/johnhenry/safe-fragment.git#<sha>. dist/ is not committed, so the package has a prepare
script that builds it for git installs and npm ci (after installing its devDependencies; it skips with a warning instead of
failing when devDependencies are omitted). Before that script
(safe-fragment#10, 1817d79) a git install was an empty package. A
registry tarball is never built this way. The built dist/index.js still reaches the fallback with a bare import("dompurify"),
so a page that imports it from node_modules needs the import map too.
Register the element
Section titled “Register the element”import { registerSafeFragment } from "@johnhenry/safe-fragment";
// Call once, from browser-executed code. It never happens as an import side effect.registerSafeFragment();registerSafeFragment() is idempotent for a tag name, and throws UNSUPPORTED_ENVIRONMENT where there is no DOM. Its options
(fetch, maxInputLength, loadDOMPurify, tagName) are in the API reference.
First render
Section titled “First render”Set the .html property from script:
<safe-fragment profile="article-v1" id="post"></safe-fragment><script type="module"> document.getElementById("post").html = await fetch("/api/posts/42").then((r) => r.text());</script>Or declaratively, through a <template> child. <template> content is inert until explicitly read, so the browser never
eagerly parses it as live markup:
<safe-fragment profile="article-v1"> <template> <p>Hello <strong>world</strong>. <img src="x" onerror="alert(1)" /></p> </template></safe-fragment>The onerror attribute is removed and nothing executes. The profile attribute is required: there is no default profile,
and a missing or unknown one rejects with UNKNOWN_PROFILE. See the element for the source
precedence and profiles for choosing one.
To see what a render changed, listen for safe-fragment:render, whose detail.report is a SanitizationReport:
post.addEventListener("safe-fragment:render", (e) => console.log(e.detail.report.removedAttributes));No bundler: the import map
Section titled “No bundler: the import map”The fallback engine does import("dompurify"), a bare specifier. With a bundler it just resolves. With none (a static page,
<script type="module">), map it, pinning the exact version this release pins:
<script type="importmap"> { "imports": { "dompurify": "https://cdn.jsdelivr.net/npm/[email protected]/dist/purify.es.mjs" } }</script>or hand safe-fragment the factory yourself:
registerSafeFragment({ loadDOMPurify: () => import("https://cdn.jsdelivr.net/npm/[email protected]/dist/purify.es.mjs").then((m) => m.default),});If neither is in place and the browser needs the fallback, rendering rejects with SANITIZER_UNAVAILABLE, whose message says
exactly this. Call await preloadSanitizer() at startup to pay the load once and surface the problem early. It resolves
"native" without loading anything where setHTML exists, and it is what makes the synchronous API usable on Safari. See
the sanitize API.
With mport: on raw-file CDNs (jsDelivr, unpkg) the generated import map only contains
the entry points you ask for, so list dompurify explicitly, or let mport add it from this package’s dependencies with
--dependencies (build(specs, { dependencies: true }) from code):
Use the exact version this release pins. Every page in the repository’s examples carries an import map for it.