| 1 | # UI Prototype |
| 2 | |
| 3 | Generate **several radically different UI variations** on a single route, switchable from a floating bottom bar. The user flips between variants in the browser, picks one (or steals bits from each), then throws the rest away. |
| 4 | |
| 5 | If the question is about logic/state rather than what something looks like — wrong branch. Use [LOGIC.md](LOGIC.md). |
| 6 | |
| 7 | ## When this is the right shape |
| 8 | |
| 9 | - "What should this page look like?" |
| 10 | - "I want to see a few options for this dashboard before committing." |
| 11 | - "Try a different layout for the settings screen." |
| 12 | - Any time the user would otherwise spend a day picking between three vague mockups in their head. |
| 13 | |
| 14 | ## Two sub-shapes — strongly prefer sub-shape A |
| 15 | |
| 16 | A UI prototype is much easier to judge when it's **butting up against the rest of the app** — real header, real sidebar, real data, real density. A throwaway route on its own is a vacuum: every variant looks fine in isolation. Default to sub-shape A whenever there's a plausible existing page to host the variants. Only reach for sub-shape B if the prototype genuinely has no nearby home. |
| 17 | |
| 18 | ### Sub-shape A — adjustment to an existing page (preferred) |
| 19 | |
| 20 | The route already exists. Variants are rendered **on the same route**, gated by a `?variant=` URL search param. The existing data fetching, params, and auth all stay — only the rendering swaps. This is the default; pick it unless there's a specific reason not to. |
| 21 | |
| 22 | If the prototype is for something that doesn't yet have a page but *would naturally live inside one* (a new section of the dashboard, a new card on the settings screen, a new step in an existing flow) — that's still sub-shape A. Mount the variants inside the host page. |
| 23 | |
| 24 | ### Sub-shape B — a new page (last resort) |
| 25 | |
| 26 | Only use this when the thing being prototyped genuinely has no existing page to live inside — e.g. an entirely new top-level surface, or a flow that can't be embedded anywhere sensible. |
| 27 | |
| 28 | Create a **throwaway route** following whatever routing convention the project already uses — don't invent a new top-level structure. Name it so it's obviously a prototype (e.g. include the word `prototype` in the path or filename). Same `?variant=` pattern. |
| 29 | |
| 30 | Before committing to sub-shape B, sanity-check: is there really no existing page this could be embedded in? An empty route hides design problems that a populated one would expose. |
| 31 | |
| 32 | In both sub-shapes the floating bottom bar is identical. |
| 33 | |
| 34 | ## Process |
| 35 | |
| 36 | ### 1. State the question and pick N |
| 37 | |
| 38 | Default to **3 variants**. More than 5 stops being radically different and starts being noise — cap there. |
| 39 | |
| 40 | Write down the plan in one line, in the prototype's location or a top-of-file comment: |
| 41 | |
| 42 | > "Three variants of the settings page, switchable via `?variant=`, on the existing `/settings` route." |
| 43 | |
| 44 | This works whether the user is here to push back or not. |
| 45 | |
| 46 | ### 2. Generate radically different variants |
| 47 | |
| 48 | Draft each variant. Hold each one to: |
| 49 | |
| 50 | - The page's purpose and the data it has access to. |
| 51 | - The project's component library / styling system (TailwindCSS, shadcn, MUI, plain CSS, whatever). |
| 52 | - A clear exported component name, e.g. `VariantA`, `VariantB`, `VariantC`. |
| 53 | |
| 54 | Variants must be **structurally different** — different layout, different information hierarchy, different primary affordance, not just different colours. Three slightly-tweaked card grids isn't a UI prototype, it's wallpaper. If two drafts come out too similar, redo one with explicit "do not use a card grid" guidance. |
| 55 | |
| 56 | ### 3. Wire them together |
| 57 | |
| 58 | Create a single switcher component on the route: |
| 59 | |
| 60 | ```tsx |
| 61 | // pseudo-code — adapt to the project's framework |
| 62 | const variant = searchParams.get('variant') ?? 'A'; |
| 63 | return ( |
| 64 | <> |
| 65 | {variant === 'A' && <VariantA {...data} />} |
| 66 | {variant === 'B' && <VariantB {...data} />} |
| 67 | {variant === 'C' && <VariantC {...data} />} |
| 68 | <PrototypeSwitcher variants={['A','B','C']} current={variant} /> |
| 69 | </> |
| 70 | ); |
| 71 | ``` |
| 72 | |
| 73 | For sub-shape A (existing page): keep all the existing data fetching above the switcher; only the rendered subtree changes per variant. |
| 74 | |
| 75 | For sub-shape B (new page): the throwaway route under `/prototype/<name>` mounts the same switcher. |
| 76 | |
| 77 | ### 4. Build the floating switcher |
| 78 | |
| 79 | A small fixed-position bar at the bottom-centre of the screen with three pieces: |
| 80 | |
| 81 | - **Left arrow** — cycles to the previous variant (wraps around). |
| 82 | - **Variant label** — shows the current variant key and, if the variant exports a name, that name too. e.g. `B — Sidebar layout`. |
| 83 | - **Right arrow** — cycles forward (wraps around). |
| 84 | |
| 85 | Behaviour: |
| 86 | |
| 87 | - Clicking an arrow updates the URL search param (use the framework's router — `router.replace` on Next, `navigate` on React Router, etc) so the variant is shareable and reload-stable. |
| 88 | - Keyboard: `←` and `→` arrow keys also cycle. Don't intercept arrow keys when an `<input>`, `<textarea>`, or `[contenteditable]` is focused. |
| 89 | - Visually distinct from the page (e.g. high-contrast pill, subtle shadow) so it's obviously not part of the design being evaluated. |
| 90 | - Hidden in production builds — gate on `process.env.NODE_ENV !== 'production'` or an equivalent check, so a stray prototype merge can't ship the bar to users. |
| 91 | |
| 92 | Put the switcher in a single shared component so both sub-shapes can reuse it. Locate it wherever shared UI lives in the project. |
| 93 | |
| 94 | ### 5. Hand it over |
| 95 | |
| 96 | Surface the URL (and the `?variant=` keys). The user will flip through whenever they get to it. The interesting feedback is usually **"I want the header from B with the sidebar from C"** — that's the actual design they want. |
| 97 | |
| 98 | ### 6. Capture the answer and clean up |
| 99 | |
| 100 | Once a variant has won, write down which one and why (commit message, ADR, issue, or a `NOTES.md` next to the prototype if running AFK and the user hasn't responded yet). Then: |
| 101 | |
| 102 | - **Sub-shape A** — delete the losing variants and the switcher; fold the winner into the existing page. |
| 103 | - **Sub-shape B** — promote the winning variant to a real route, delete the throwaway route and the switcher. |
| 104 | |
| 105 | Don't leave variant components or the switcher lying around. They rot fast and confuse the next reader. |
| 106 | |
| 107 | ## Anti-patterns |
| 108 | |
| 109 | - **Variants that differ only in colour or copy.** That's a tweak, not a prototype. Real variants disagree about structure. |
| 110 | - **Sharing too much code between variants.** A shared `<Header>` is fine; a shared `<Layout>` defeats the point. Each variant should be free to throw out the layout. |
| 111 | - **Wiring variants to real mutations.** Read-only prototypes are fine. If a variant needs to mutate, point it at a stub — the question is "what should this look like", not "does the backend work". |
| 112 | - **Promoting the prototype directly to production.** The variant code was written under prototype constraints (no tests, minimal error handling). Rewrite it properly when you fold it in. |
| 113 |