Skill Tracer
Skills › Skills For Real Engineers › prototype › UI.md

UI.md

112 lines · skills/engineering/prototype/UI.md at 4ae2317

Open on GitHub
1# UI Prototype
2
3Generate **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
5If the question is about logic/state rather than what something looks like, this is the wrong branch. Use [LOGIC.md](LOGIC.md).
6

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, this is the wrong branch. Use LOGIC.md.

Raw lines 1 to 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.

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.
Raw lines 7 to 13
14## Two sub-shapes: strongly prefer sub-shape A
16A 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.

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.

Raw lines 14 to 17
18### Sub-shape A: adjustment to an existing page (preferred)
20The 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.
22If 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), it's still sub-shape A. Mount the variants inside the host page.

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), it's still sub-shape A. Mount the variants inside the host page.

Raw lines 18 to 23
24### Sub-shape B: a new page (last resort)
26Only 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).
28Create 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.
30Before 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.
32In both sub-shapes the floating bottom bar is identical.

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.

Raw lines 24 to 33
34## Process
36### 1. State the question and pick N
38Default to **3 variants**. More than 5 stops being radically different and starts being noise, so cap there.
40Write down the plan in one line, in the prototype's location or a top-of-file comment:
42> "Three variants of the settings page, switchable via `?variant=`, on the existing `/settings` route."
44This works whether the user is here to push back or not.

1. State the question and pick N

Default to 3 variants. More than 5 stops being radically different and starts being noise, so 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.

Raw lines 36 to 45
46### 2. Generate radically different variants
48Draft each variant. Hold each one to:
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`.
54Variants 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.

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.

Raw lines 46 to 55
56### 3. Wire them together
58Create a single switcher component on the route:
60```tsx
61// pseudo-code, adapt to the project's framework
62const variant = searchParams.get('variant') ?? 'A';
63return (
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```
73For sub-shape A (existing page): keep all the existing data fetching above the switcher; only the rendered subtree changes per variant.
75For sub-shape B (new page): the throwaway route under `/prototype/<name>` mounts the same switcher.

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.

Raw lines 56 to 76
77### 4. Build the floating switcher
79A small fixed-position bar at the bottom-centre of the screen with three pieces:
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).
85Behaviour:
87- Clicking an arrow updates the URL search param (use the framework's router, e.g. `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.
92Put the switcher in a single shared component so both sub-shapes can reuse it. Locate it wherever shared UI lives in the project.

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, e.g. 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.

Raw lines 77 to 93
94### 5. Hand it over
96Surface 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"**, which is the actual design they want.

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", which is the actual design they want.

Raw lines 94 to 97
98### 6. Capture the answer and clean up
100Once a variant has won, capture the answer (which variant and why), then capture the prototype the way the [SKILL](SKILL.md) describes. Fold the winner into the real code and move the rest onto the throwaway branch, not into main:
102- **Sub-shape A**: fold the winner into the existing page; drop the losing variants and the switcher from main.
103- **Sub-shape B**: promote the winning variant to a real route; drop the throwaway route and the switcher from main.
105The full set of variants is the primary source, so it lands on the throwaway branch, not the bin, since variant components and the switcher left in the main branch rot fast and confuse the next reader.

6. Capture the answer and clean up

Once a variant has won, capture the answer (which variant and why), then capture the prototype the way the SKILL describes. Fold the winner into the real code and move the rest onto the throwaway branch, not into main:

  • Sub-shape A: fold the winner into the existing page; drop the losing variants and the switcher from main.
  • Sub-shape B: promote the winning variant to a real route; drop the throwaway route and the switcher from main.

The full set of variants is the primary source, so it lands on the throwaway branch, not the bin, since variant components and the switcher left in the main branch rot fast and confuse the next reader.

Raw lines 98 to 106
107## Anti-patterns
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.

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.
Raw lines 107 to 112