7. Multi-substrate side-by-side¶
re-frame2 ships with four view-substrate adapters: Reagent, Reagent-slim, UIx, and Helix. Most apps pick one and stay there. But for component-library maintainers and adapter authors, the same variant rendered under multiple substrates is the daily diff.
Story's :substrates slot on reg-story declares which substrates the parent story exercises:
(story/reg-story :story.counter
{:component :counter-with-stories.views/counter-card
:substrates #{:reagent :uix :helix}
:args {:label "Count"}})
Now every variant under :story.counter mounts under all three substrates, side-by-side, in a tri-cell layout.
What you see¶
Three columns. Same variant. Three substrates rendering it. The component must be substrate-agnostic — keyword view-id, registered through reg-view, no substrate-specific imports in the view body. The runtime knows how to invoke the keyword under each substrate.

If a substrate-specific behaviour diverges (e.g., Reagent's r-atom mount semantics vs UIx's signal mount), the three columns diverge visibly. That's the point of the side-by-side: visual confirmation of behaviour parity.
The contract on substrate-agnostic views¶
For :substrates to work, your view registration must be substrate-agnostic. Concretely:
- No
:requireof substrate-specific libraries from inside the view body. (reagent.core,uix.core,helix.dom— none of them.) - Use
reg-view(not Reagent'sdefnshape or UIx'sdefuishape directly).reg-viewis the substrate-agnostic registration macro; the adapters interpret the registered body. - For host-specific affordances (Reagent's
:>shape, UIx's$shape), guard them behind a(case (rf/substrate) :reagent ... :uix ...)switch. Most apps don't need this.
Views that do need substrate-specific code aren't covered by :substrates; they live under a single-substrate parent story. The adapter chapters at Guide 19 — Adapters walk the gotchas.
What the side-by-side diff is for¶
Three audiences reach for this:
- Adapter authors. When you're writing a new adapter (or fixing a regression in an existing one), you compare the same variant under your new adapter and the canonical Reagent. Visual divergence = bug in the new adapter. The diff is per-variant, per-arg-tuple, per-mode — a tiny budget for differences.
- Component-library maintainers. A re-frame2 component library that ships across substrates lives or dies on whether
:re-com/dropdownrenders the same way under all four. Side-by-side is the regression test. - Apps migrating substrates. When a project moves from Reagent v1 to Reagent v2, or from Reagent to Reagent-slim for bundle-size reasons, side-by-side variants are the safety net.
For most app code, you don't need this. You pick a substrate at init! time and your stories run under that substrate. Multi-substrate is for the cases where multi-substrate is the point.
What's deferred¶
Two surfaces aren't in v1.0:
- Per-substrate args. You can't currently say "use these args under Reagent and those args under UIx" — args are substrate-agnostic. If the substrates need different args to produce the same output, the registered view is doing something host-specific that probably should be factored out.
- Cross-substrate snapshot identity. Today every substrate produces its own snapshot identity (because the rendered DOM is hashed). A future
:semantic-fingerprintslot would let visual-regression diff intended output rather than rendered output. The spec rev is open; today the right tool is human visual review.
That's Story.
You've seen:
- The three load-bearing rules: frame-per-variant, EDN bodies, record-don't-throw.
- The four daily affordances: variants, mode tabs, recorder, workspaces.
- The three surfaces unusual for the Storybook lineage: snapshot identity, time-travel, multi-substrate.
- The agent surface (story-mcp) that consumes all of the above through MCP tools.
If you want to look at the worked example end-to-end, tools/story/testbeds/counter_with_stories/ wires the seven reg-* macros against the canonical counter app — four variants, two workspaces, every assertion shape, every decorator kind, plus a passing integration test.
Or open #/stories against your own app, register one variant, and see how it feels.