Skip to content

Web component

<sfn-diagram> is a custom element shipped from the sfn-diagram/element subpath — no extra package. It works anywhere custom elements do: Vue, Svelte, Solid, Angular, Astro, Hono, plain HTML/htmx, and React 19+. It renders in light DOM (no shadow root), so page CSS reaches the SVG, and it degrades to plain server-rendered markup when there’s no client-side JavaScript at all.

Terminal window
npm i sfn-diagram

Import sfn-diagram/element/auto for the zero-config default — it registers <sfn-diagram> as a side effect:

<script type="module" src="sfn-diagram/element/auto"></script>
<sfn-diagram interactive theme="dark"></sfn-diagram>
<script type="module">
document.querySelector('sfn-diagram').definition = myAslDefinition;
</script>

Import sfn-diagram/element directly (no /auto) and call defineSfnDiagram yourself for a different tag name — the custom element spec allows registering one class under only one tag name, ever, so only one of the two should run per page:

import { defineSfnDiagram } from 'sfn-diagram/element';
defineSfnDiagram({ name: 'my-diagram' }); // <my-diagram> instead of <sfn-diagram>
Attribute Property Type Default Notes
definition definition AslDefinition | string — (required) Attribute accepts a JSON string; the property also accepts an object
— history ExecutionHistoryInput — Property-only — objects don’t survive an attribute round-trip. Renders an execution overlay when set
format format 'svg' | 'mermaid' 'svg'
layout layout 'TB' | 'LR' | 'RL' | 'BT' 'TB'
theme theme 'light' | 'dark' | CustomTheme 'light' Attribute accepts 'light'/'dark' only; set the property for a CustomTheme object
interactive interactive boolean false Enables pan/zoom, state search, the minimap, and the detail panel for a clicked state or edge

An embedded viewer sits inside a page that scrolls, so it doesn’t take over the mouse wheel or touch gestures until you engage with it — click or tap the diagram, or move keyboard focus into it — and gives them back once you click elsewhere on the page. The one exception is Ctrl+wheel, which is also what a trackpad pinch sends: pinching over the diagram zooms the diagram at any time, engaged or not, since that gesture is never an attempt to scroll the page.

Failure to parse or validate definition dispatches a bubbling sfn-error event (CustomEvent<Error>) and renders nothing:

document.querySelector('sfn-diagram').addEventListener('sfn-error', (event) => {
console.error(event.detail); // Error
});

Every framework in Use with your framework can set properties on a custom element directly — React 19+, Vue, Svelte, Solid, and Angular all pass custom-elements-everywhere’s full test suite, so .definition = asl (not the definition attribute) works the same way it would on any other DOM element:

// React 19+ — no wrapper needed, unlike React 18 (use sfn-diagram-react there instead)
<sfn-diagram ref={(el) => { if (el) el.definition = asl; }} interactive />
<!-- Vue 3 -->
<sfn-diagram :definition="asl" interactive />
<!-- Svelte 5 -->
<sfn-diagram bind:this={el} interactive />

Omit definition and give the element a pre-rendered <svg> child instead — from generateSvg() run at build or request time — and it’s left untouched:

---
import { generateSvg } from 'sfn-diagram';
const { svg } = generateSvg({ aslDefinition: definition });
---
<sfn-diagram set:html={svg} />

Adding interactive on top of server-rendered markup still upgrades it with pan/zoom and search once the element’s JavaScript loads — the detail panel is the one exception, since it needs the original ASL client-side; set definition alongside the pre-rendered SVG if you need it too.

Every internal id the interactive viewer needs is scoped with [data-sfn="..."] attributes rather than global ids, and each element’s own SVG marker defs are namespaced per instance — so two (or two hundred) <sfn-diagram interactive> elements on the same page don’t collide.

A custom-elements.json is published alongside the package (sfn-diagram’s customElements field), for editor autocomplete, JSX types, and generated framework wrappers via tools like wc-toolkit.