Integrations

Domphy works with most JavaScript/TypeScript libraries because it keeps a strict boundary:

  • UI is just declarative element objects rendered by ElementNode.
  • Reactivity is subscription-based (listener) and can be connected to any external store/event source.
  • State architecture is not owned by Domphy.

See related references:

Why It Works With Most JS/TS Libraries

Domphy does not force a global app model, router model, or data cache model.
If a library can notify changes, it can drive Domphy UI.

// External store -> Domphy listener
store.subscribe(() => listener())

// External stream -> Domphy listener
stream.subscribe(() => listener())

This keeps integration simple: use your preferred state architecture, and only bridge updates at the view edge.

Boundary Rule: Data and UI Must Stay Separate

Domphy does not encourage "plugin islands" for framework-level behavior, especially plugins that combine data/state orchestration with UI behavior.

Why:

  • It increases abstraction layers with little practical value.
  • It blurs the boundary between data flow and presentation.
  • It makes long-term maintenance harder, especially across teams.

The author previously built plugin-style integrations (including query/router wrappers), then removed them for this reason: the wrappers added abstraction but reduced clarity around ownership of data vs UI.

  1. Keep data/state in the external library (query client, router, store, stream, etc.).
  2. Convert only the minimum needed signals into Domphy-reactive reads at render points.
  3. Keep patch/component code focused on presentation and interaction.

Domphy package, or vanilla?

Domphy ships first-party packages only for the cores that benefit from a tight Domphy-reactivity adapter. Everything else you use vanilla, directly — there is intentionally no @domphy/chart, @domphy/i18n, @domphy/editor, etc. Domphy is better at this than React: lifecycle hooks (_onMount/_onRemove) integrate imperative DOM libraries cleanly, with no virtual DOM fighting them.

NeedUse
async data / tables / routing / virtualization / forms@domphy/query · @domphy/table · @domphy/router · @domphy/virtual · @domphy/form (1-1 TanStack ports + adapter)
drag & drop@domphy/dnd
animationthe motion() patch (@domphy/ui)
chartsvanilla Chart.js / ECharts / D3
rich textvanilla TipTap / ProseMirror / Lexical (framework-agnostic cores)
carouselvanilla embla-carousel (its core is framework-agnostic)
i18nvanilla i18next (core) — recipe
datesvanilla dayjs / date-fns / flatpickr
schema validationvanilla zod (works with @domphy/form via Standard Schema) — recipe
maps / 3Dvanilla leaflet/maplibre · three.js
iconsany SVG string + the icon() patch (e.g. lucide icons)

If a library has a framework-agnostic core (most do — the "React" version is usually a thin wrapper), use that core. No wrapper needed.

DOM library pattern

The canonical way to mount any imperative DOM library:

{
  div: null, // the library's mount target
  _onMount: (node) => {
    const instance = new SomeLib(node.domElement, options)
    node.setMetadata("lib", instance)
  },
  _onRemove: (node) => {
    (node.getMetadata("lib") as SomeLib | undefined)?.destroy()
  },
}

When the library mutates the DOM itself (e.g. a drag-sort plugin reorders nodes), sync Domphy's logical tree without re-touching the DOM using node.children.move(from, to, /* updateDom */ false). React can't do this — its virtual DOM must own the tree; Domphy keeps tree and DOM in sync independently.

Examples