Skip to content

Latest commit

 

History

History
112 lines (69 loc) · 6.63 KB

File metadata and controls

112 lines (69 loc) · 6.63 KB

UI Prototype

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.

If the question is about logic/state rather than what something looks like — wrong branch. Use LOGIC.md.

When this is the right shape

  • "What should this page look like?"
  • "I want to see a few options for this dashboard before committing."
  • "Try a different layout for the settings screen."
  • Any time the user would otherwise spend a day picking between three vague mockups in their head.

Two sub-shapes — strongly prefer sub-shape A

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.

Sub-shape A — adjustment to an existing page (preferred)

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.

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.

Sub-shape B — a new page (last resort)

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.

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.

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.

In both sub-shapes the floating bottom bar is identical.

Process

1. State the question and pick N

Default to 3 variants. More than 5 stops being radically different and starts being noise — cap there.

Write down the plan in one line, in the prototype's location or a top-of-file comment:

"Three variants of the settings page, switchable via ?variant=, on the existing /settings route."

This works whether the user is here to push back or not.

2. Generate radically different variants

Draft each variant. Hold each one to:

  • The page's purpose and the data it has access to.
  • The project's component library / styling system (TailwindCSS, shadcn, MUI, plain CSS, whatever).
  • A clear exported component name, e.g. VariantA, VariantB, VariantC.

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.

3. Wire them together

Create a single switcher component on the route:

// pseudo-code — adapt to the project's framework
const variant = searchParams.get('variant') ?? 'A';
return (
  <>
    {variant === 'A' && <VariantA {...data} />}
    {variant === 'B' && <VariantB {...data} />}
    {variant === 'C' && <VariantC {...data} />}
    <PrototypeSwitcher variants={['A','B','C']} current={variant} />
  </>
);

For sub-shape A (existing page): keep all the existing data fetching above the switcher; only the rendered subtree changes per variant.

For sub-shape B (new page): the throwaway route under /prototype/<name> mounts the same switcher.

4. Build the floating switcher

A small fixed-position bar at the bottom-centre of the screen with three pieces:

  • Left arrow — cycles to the previous variant (wraps around).
  • Variant label — shows the current variant key and, if the variant exports a name, that name too. e.g. B — Sidebar layout.
  • Right arrow — cycles forward (wraps around).

Behaviour:

  • 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.
  • Keyboard: and arrow keys also cycle. Don't intercept arrow keys when an <input>, <textarea>, or [contenteditable] is focused.
  • Visually distinct from the page (e.g. high-contrast pill, subtle shadow) so it's obviously not part of the design being evaluated.
  • 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.

Put the switcher in a single shared component so both sub-shapes can reuse it. Locate it wherever shared UI lives in the project.

5. Hand it over

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.

6. Capture the answer and clean up

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:

  • Sub-shape A — delete the losing variants and the switcher; fold the winner into the existing page.
  • Sub-shape B — promote the winning variant to a real route, delete the throwaway route and the switcher.

Don't leave variant components or the switcher lying around. They rot fast and confuse the next reader.

Anti-patterns

  • Variants that differ only in colour or copy. That's a tweak, not a prototype. Real variants disagree about structure.
  • 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.
  • 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".
  • 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.