# Multi-Framework Support > 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. ```html Promote prototype ``` That tag is valid in: - A Django template - A React JSX file - A Vue Single-File Component - A Svelte component - `index.html` with no JS framework at all - An HTMX `hx-get` fragment response 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). --- ## Architecture recap ``` ┌─────────────────────────────────────────────────────────────┐ │ Layer 3 — Framework adapters (optional) │ │ Django {% include %} partials, Vue/React typed wrappers │ ├─────────────────────────────────────────────────────────────┤ │ Layer 2 — Custom elements (canonical) │ │ , , , … │ │ Lit-based, light DOM, ~5kb gzipped runtime total │ ├─────────────────────────────────────────────────────────────┤ │ Layer 1 — Tokens + CSS │ │ colors_and_type.css + components.css │ └─────────────────────────────────────────────────────────────┘ ``` 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 render() { return html``; } } ``` Consequences: 1. **Global stylesheets style them.** Your `components.css` rules for `.wn-btn` apply to the button inside ``. No `::part`, no `::slotted`, no CSS variables-as-API. 2. **SSR is trivial.** The server renders the same `` 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.** `` containing a real `` participates in `
` 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 `` 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. --- ## Per-framework setup ### Plain HTML The minimum viable setup. No build step. ```html Concierge prototype triage Request invite ``` Vendor the three files (`colors_and_type.css`, `components.css`, `index.js`) into your static directory. That's the whole integration. --- ### Django (server-rendered templates + HTMX) This is the canonical non-React case and worth covering in detail. #### 1. Install ```sh # In the Django app's repo. pnpm add git+ssh://git@gitea.example.com/whynot/whynot-design.git#v0.2.0 # Or vendor without Node tooling — copy three files: mkdir -p myapp/static/whynot cp node_modules/@whynot/design/src/styles/colors_and_type.css myapp/static/whynot/ cp node_modules/@whynot/design/src/styles/components.css myapp/static/whynot/ cp node_modules/@whynot/design/dist/index.bundle.js myapp/static/whynot/ ``` A small `Makefile` or `package.json` script makes this idempotent — re-run after every dependency bump: ```makefile sync-whynot: cp node_modules/@whynot/design/src/styles/*.css myapp/static/whynot/ cp node_modules/@whynot/design/dist/*.js myapp/static/whynot/ ``` #### 2. Wire it into your base template ```django {# templates/base.html #} {% load static %} … {% block content %}{% endblock %} ``` That's it. From now on, **any Django template** can use `` elements with no further setup. #### 3. Use components in templates ```django {# templates/prototypes/detail.html #} {% extends "base.html" %} {% block content %}
Prototypes {{ prototype.id }} {{ prototype.pitch }} Park Promote → {{ prototype.target }} {{ prototype.learning_question }} {{ prototype.smallest_test }} {{ prototype.risks }}
{% endblock %} ``` No `{% include %}` files needed for this to work. The components are *real HTML*; Django renders them like any other tag. #### 4. HTMX fragment swaps When HTMX swaps a fragment into the page, the new HTML contains `` elements. The browser upgrades them automatically — no extra wiring. ```django {# views.py #} def signal_create(request): if request.method == "POST": sig = Signal.objects.create(…) return render(request, "signals/_row.html", {"signal": sig}) # fragment return render(request, "signals/_form.html") {# templates/signals/_row.html — the fragment HTMX swaps in #} {{ signal.id }} {{ signal.what }} ``` ```django {# Where HTMX swaps it in #} … ``` The new row's `` is upgraded on insertion. No `htmx:afterSettle` listener needed. #### 5. Django partials (optional Layer 3) If a particular component pattern recurs and the raw HTML is verbose, ship a `{% include %}` partial: ```django {# adapters/django/templates/whynot/_prototype_card.html — vendored in your project #} {{ p.pitch }} {{ p.learning }} {{ p.test }} → {{ p.target }} ``` Then: ```django {% 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 ``, ``, `` each wrap a real native input. Django's form rendering works as expected: ```django
{% csrf_token %} Save
``` 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) import "@whynot/design/styles/colors_and_type.css"; import "@whynot/design/styles/components.css"; import "@whynot/design"; // registers all custom elements ``` #### 3. Use components in JSX ```jsx export default function PrototypeDetailPage({ prototype }) { return (
{prototype.pitch} promote(prototype)}> Promote {prototype.learningQuestion}
); } ``` #### 4. React-specific notes - **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 (``). - **Events.** React 18 doesn't bind `onMyEvent`-style props to custom-element events. Use `useEffect` + `addEventListener` for component-emitted events: ```jsx const ref = useRef(); useEffect(() => { const el = ref.current; el?.addEventListener("wn-dismiss", handleDismiss); return () => el?.removeEventListener("wn-dismiss", handleDismiss); }, []); return Saved.; ``` React 19 supports `onWnDismiss` directly. - **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 `` over ``, one-line wrappers are trivial: ```jsx // myapp/lib/wn.jsx export const WnButton = (props) => ; export const WnCard = (props) => ; ``` 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 ``` ```svelte Promote ``` 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), `` 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 `` 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 `` already work as inert static HTML if JS never loads. Add `