Core concepts
How Whichly renders variants, encodes state in the URL, and isolates the picker.
Whichly is built on three ideas. Understanding them explains both what it does well and where its constraints come from.
Render-all, hide with CSS
Every <Variant> you declare is always present in the React tree. Whichly doesn't unmount the
inactive ones — it hides them with CSS:
- The active variant gets
display: contents, so its wrapper adds no layout box. The variant's children participate directly in the parent's flex/grid layout, exactly as if the<Variant>wrapper weren't there. - The inactive variants get
display: none.
Each rendered variant also carries data-vp-block and data-vp-variant attributes, which is
handy for debugging or targeting a specific variant in styles or tests.
Because all variants render, switching between them is instant — there's no remount, no data refetch, no layout jump. The trade-off is that every variant's component tree runs (effects, data hooks, etc.), so keep that in mind for expensive variants.
URL-encoded, shareable state
The current selection lives entirely in the URL query string under the vp parameter:
?vp=Hero:Friendly,CTA:SubtleWhen the provider mounts it parses this parameter to restore the selection. Each time someone changes a variant, Whichly writes the new state back to the URL. That means a chosen combination is just a link — no database, no session, no server round-trip. Copy the URL and the recipient sees exactly what you saw.
See Shareable URLs for the exact format.
Shadow-DOM-isolated picker
The picker UI is mounted through a React portal into a shadow root appended to
document.body. Its styles (Tailwind + a small vendored component set) are compiled to a CSS
string and injected into that shadow root.
This gives complete two-way isolation:
- The picker's CSS cannot leak onto your page.
- Your page's CSS cannot leak into the picker.
It's why there's no stylesheet for you to import and no risk of class-name collisions, no matter what CSS framework your host page uses.
One package, one tree
There's no CDN script and no separate runtime to load. WhichlyProvider, Block, Variant,
and WhichlyPicker are all part of the same React tree in your app. The only thing that escapes
the tree is the picker portal into the shadow root — and that's mounted for you automatically.