> How to consume `@whynot/design` from React, Django, Vue, Svelte, or plain HTML — without forking, restyling, or re-implementing components per framework.
---
## TL;DR
`whynot-design` ships **Web Components** (custom elements built with Lit) as the canonical component layer. They render to **light DOM** — meaning the global stylesheets style them — and work identically in any HTML context.
You do not write a different component per framework. You write the same custom element. The framework decides how to pass props (`variant="primary"` in HTML, `variant="primary"` in JSX, `:variant="…"` in Vue).
Most consumers need only Layer 1 + Layer 2. Layer 3 is sugar.
---
## How light DOM rendering works
A Lit component normally renders into a **shadow root** — its own isolated DOM tree with style encapsulation. That's wrong for a design system: it would prevent the global CSS from styling components, force every component to ship its own copy of the rules, and break server-side rendering.
`whynot-design` components override the render root to be light DOM:
```js
class WnButton extends LitElement {
createRenderRoot() { return this; } // ← light DOM
1.**Global stylesheets style them.** Your `components.css` rules for `.wn-btn` apply to the button inside `<wn-button>`. No `::part`, no `::slotted`, no CSS variables-as-API.
2.**SSR is trivial.** The server renders the same `<wn-button><button class="wn-btn">…</button></wn-button>` HTML it would render for any tag. The browser later "upgrades" the element to add behaviour, but the HTML is already there — no FOUC, no hydration mismatch.
3.**Form participation works.**`<wn-input>` containing a real `<input>` participates in `<form>` submission naturally. No ElementInternals contortions.
4.**HTMX swaps just work.** When HTMX replaces a fragment of the page with new HTML from the server, any `<wn-*>` elements in the new fragment are upgraded automatically by the browser's `connectedCallback`.
The trade-off you give up is style isolation. For a design system, that's the right trade — the system *wants* its CSS to apply everywhere.
{% include "whynot/_prototype_card.html" with p=prototype %}
```
`whynot-design` ships a starter set of these in `adapters/django/templates/whynot/`. **Copy them into your Django app's templates dir; don't add `adapters/django/templates/` to `TEMPLATES.DIRS` directly** — that would couple your `INSTALLED_APPS` to the design system's repo layout. Treat the partials as starting templates you can customise per-app.
#### 6. Form participation
`<wn-input>`, `<wn-textarea>`, `<wn-select>` each wrap a real native input. Django's form rendering works as expected:
The `name` attribute reaches the native input inside; the form submits normally.
---
### React (Next.js, Vite, CRA, Remix)
React supports custom elements natively since v19; in v18 the support is mostly fine for the common cases (attributes-not-properties, no event handlers via props). The whynot components are designed against the v18 baseline.
#### 1. Install
```sh
pnpm add @whynot/design
```
#### 2. Wire it into your app entry
```jsx
// app/layout.jsx (Next.js) or src/main.jsx (Vite)
- **Attributes vs properties.** React passes string-typed props as attributes, which is what whynot components expect for all public APIs (`variant`, `signal`, `active-idx`). Boolean attributes work too (`<wn-tag active>`).
- **Events.** React 18 doesn't bind `onMyEvent`-style props to custom-element events. Use `useEffect` + `addEventListener` for component-emitted events:
- **JSX typing.** If you use TypeScript, add a `whynot.d.ts` declaring the custom elements as JSX intrinsic elements. `@whynot/design` does not ship this yet (deferred — see CHANGELOG).
#### 5. Typed React wrappers (Layer 3, optional)
If your team prefers `<WnButton variant="primary">` over `<wn-button variant="primary">`, one-line wrappers are trivial:
Whether `whynot-design` ships these officially is on the deferred list — most React teams find them unnecessary once the IDE handles the custom-element completion.
---
### Vue, Svelte, SolidJS
All work out of the box; custom elements are platform features, not framework features.
Vue may warn about unknown elements unless you configure `compilerOptions.isCustomElement` to recognise `wn-`-prefixed tags. One-line vite plugin config.
---
## Server-side rendering specifics
### When SSR matters
For Django (or any server-rendered framework), `<wn-button>` reaches the browser as inert markup. It's *upgraded* — gains interactive behaviour — when the browser parses `index.js` and runs `customElements.define()`. Between those two moments, the user may see:
- **The button** — fully styled by `components.css`, looking correct.
- **Click behaviour** — depends on the component. Pure-visual components (Eyebrow, Card, Tag) have no behaviour to gain. Interactive components (Modal, Toast, Select) need the upgrade.
### Strategies for behaviour-during-load
1.**Defer behaviour-required interaction.** A `<wn-modal>` that's initially closed doesn't care about the upgrade window. Only when the user clicks "Open" does the upgrade matter — and by then `index.js` has loaded.
2.**Block on the module if it's truly critical.** Remove `defer` from the script tag for a smaller-than-50kb runtime — acceptable. Or use `type="module"` (which defers by default but blocks `DOMContentLoaded`).
3.**Use `noscript` fallbacks.** Components like `<wn-toast>` already work as inert static HTML if JS never loads. Add `<noscript>` siblings for behaviour-only flourishes (e.g. dismiss buttons).
### Declarative Shadow DOM
Not used by `whynot-design`. Light-DOM rendering makes Declarative Shadow DOM unnecessary — the markup the server emits is the markup the browser shows.
---
## Visual regression in consumer repos
Both the design-system repo and consuming repos run their own visual-regression suites against the same components. If the design system bumps a token value and `examples/showcase/` passes (because the regression baselines were updated), but the consumer's app screenshots fail, **the consumer's CI catches the regression first**. That's the point of the dual-CI setup.
When `@whynot/design` ships `0.3.1` with a thicker hover bar, this test fails on the consumer side. Reviewer accepts the change, updates the snapshot, merges.
---
## What you don't have to worry about
- **Framework version drift.** Web Components are platform tech. A v0.3.0 component works in Lit 3, Lit 4, and post-Lit if Lit is ever replaced.
- **Multiple copies of the runtime.** Lit deduplicates at the module level. Importing `@whynot/design` from 12 consumer pages still loads one copy.
- **Bundler edge cases.** Vite, Next, Webpack 5, Rollup, esbuild all handle Lit and `customElements.define()` correctly out of the box.
- **A11y.** Components emit semantic HTML (`<button>`, `<input>`, `<dialog>`) and inherit native accessibility. Where extra ARIA is needed (e.g. `<wn-modal>` traps focus, `<wn-tag>` doesn't claim a role), it's documented inline.
---
## Open questions / known gaps
- **Native `<form>` participation for `<wn-select>`** — currently fine because the inner native `<select>` carries the `name` attribute. ElementInternals-based participation may be added for richer custom selects later.
- **Server-side declarative shadow DOM** — not needed today; light DOM covers all known consumers. Re-evaluate if a consumer ever needs strict CSS isolation per component.
- **TypeScript types for JSX** — deferred. Type declarations for the custom elements (`JSX.IntrinsicElements['wn-button']`) will ship in a `types/` folder once the first TS consumer asks.
- **Vue/Svelte adapter folders** — none ship today. Add when a real consumer appears.
| Django | Vendor 3 files into `static/whynot/`, link from `base.html` | `<wn-button>` in any template |
| HTMX | Same as Django | Fragments containing `<wn-*>` upgrade on insertion |
| React | `import "@whynot/design"` once | `<wn-button>` in JSX |
| Vue / Svelte / Solid | Same as React (one-line `isCustomElement` config in Vue) | `<wn-button>` in templates |
One canonical implementation. No re-implementation per framework. No framework lock-in.
> *whynot-design is the implementation surface — not a UI kit. The discipline that keeps it framework-agnostic is the discipline that lets it survive technology shifts.*