Claude
Skills
Sign in
Back

shadcn-prototype

Included with Lifetime
$97 forever

Use this skill when the user wants to explore design options for a shadcn/ui interface before committing to one — building 2-4 radically different UI variations on a single throwaway route, switchable via a URL search param + a floating bottom bar. Trigger phrases "let me see some options", "show me variations", "I want to play with the layout first", "prototype this", "try a few designs", "build a few mockups", "before I commit", "what should this look like", "explore the UI", "what are some ways to lay this out". Also use proactively when the user describes a new screen / page / component that has more than one reasonable layout and the right answer isn't obvious from spec alone. Different from `shadcn-components` (which assumes the design decision is made — you're adding the third button) and from a normal page build (which commits to one design). The shadcn-prototype workflow is "let me ship 3 versions side-by-side, you click through, pick one, we delete the others."

Design

What this skill does


# shadcn-prototype

Build a throwaway prototype to explore 2-4 radically different UI variations side-by-side **before** committing to one. The user clicks through, picks a winner, the chosen variant gets promoted into real code and the rest get deleted.

This is the upstream of `shadcn-components` / `shadcn-forms` / `shadcn-data-tables`. Those skills assume the design decision is made — you're filling in a known shape. This skill is *how the shape gets decided* when more than one is plausible.

## How shadcn-prototype works

The skill follows the **prototype protocol** (canonical at `docs/protocols/prototype.md` at the marketplace root; a copy ships with this plugin at `${CLAUDE_PLUGIN_ROOT}/protocols/prototype.md`). Read that file first. The protocol owns the mechanic:

- Pick a branch — UI vs Logic. shadcn-prototype is almost always **UI branch**.
- Six universal rules — throwaway-marked, one command to run, no persistence, skip the polish, surface the state, delete or absorb when done.
- Capture the answer — commit message, ADR, or `NOTES.md` before deletion.

This skill is the **shadcn-specific layering** on top of that protocol — variant-switching primitives, routing conventions, scratch persistence idioms, and concrete examples.

## When this skill is the right fit

- User describes a new screen / page / feature where >1 layout makes sense
- User says "what should this look like" or asks to mock something up
- An existing screen needs a redesign and the team isn't aligned on direction
- Design and engineering disagree on density / hierarchy / flow

When this skill is *not* the right fit:

- The team already has a Figma mock — just implement it; use `shadcn-components` directly
- The decision is trivial (button color, spacing tweak) — just ship and revise
- The question is logic, not visual — that's a different prototype shape (see `docs/protocols/prototype.md` LOGIC branch)
- A real prototype already exists in the codebase — extend rather than duplicate

## Variant-switching primitive — URL search param + floating switcher

The protocol calls for variant switching via "a URL search param + a floating bottom bar." For shadcn projects, the canonical implementation:

```tsx
// app/_prototypes/<slug>/page.tsx  (Next.js App Router)
// or src/prototypes/<slug>/index.tsx  (Vite + React Router)

"use client"
import { useSearchParams, useRouter, usePathname } from "next/navigation"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import { cn } from "@/lib/utils"

const VARIANTS = [
  { id: "single", label: "Single page", note: "Everything visible, no navigation" },
  { id: "wizard", label: "Step-by-step wizard", note: "One section per screen, next/back" },
  { id: "hybrid", label: "Collapsible single", note: "Single page, collapsible sections, skip-to-end" },
] as const

type VariantId = typeof VARIANTS[number]["id"]

export default function CheckoutPrototype() {
  const sp = useSearchParams()
  const router = useRouter()
  const pathname = usePathname()
  const active = (sp.get("v") as VariantId) || "single"

  const switchTo = (id: VariantId) => {
    const params = new URLSearchParams(sp)
    params.set("v", id)
    router.replace(`${pathname}?${params.toString()}`)
  }

  return (
    <div className="min-h-screen pb-24">
      {active === "single" && <SingleVariant />}
      {active === "wizard" && <WizardVariant />}
      {active === "hybrid" && <HybridVariant />}

      <VariantSwitcher active={active} onSwitch={switchTo} />
    </div>
  )
}

function VariantSwitcher({ active, onSwitch }: { active: VariantId; onSwitch: (id: VariantId) => void }) {
  return (
    <Card className="fixed bottom-4 left-1/2 -translate-x-1/2 flex items-center gap-2 p-2 shadow-lg">
      <span className="text-xs text-muted-foreground px-2">Prototype variants</span>
      {VARIANTS.map(v => (
        <Button
          key={v.id}
          size="sm"
          variant={active === v.id ? "default" : "ghost"}
          onClick={() => onSwitch(v.id)}
          title={v.note}
        >
          {v.label}
        </Button>
      ))}
    </Card>
  )
}
```

**Why URL search param, not React state?** Shareability. A `?v=wizard` URL can be pasted into Slack or a comment. State doesn't persist across reloads, which kills the "show me again" feedback loop.

**Why a `<Card>` floating bar, not `<Tabs>`?** Tabs imply a hierarchical relationship; prototype variants are mutually exclusive alternatives the user is choosing between. The floating bar reads as a chooser, not a navigation pattern.

**Why `Button variant="ghost" vs "default"`?** Visual signal of which variant is active without needing radio-style affordance.

## Routing conventions per framework

| Framework | Route convention | Why |
|---|---|---|
| **Next.js App Router** | `app/_prototypes/<slug>/page.tsx` | The `_` prefix doesn't actually hide it from routing in App Router, but it signals "scratch" to humans scanning the tree. Cleaner: use a `(prototypes)` route group so the URL is `/<slug>` but the file lives under `app/(prototypes)/<slug>/`. |
| **Next.js Pages Router** | `pages/prototypes/<slug>.tsx` | Standard nested routing. |
| **Vite + React Router** | A registered `<Route path="/prototypes/:slug" element={<...>} />` in your router config. File at `src/prototypes/<slug>.tsx`. |
| **Tanstack Router** | `src/routes/prototypes/<slug>.tsx` (file-based). |
| **Remix** | `app/routes/prototypes.<slug>.tsx`. |

Pick whichever matches the project's existing convention. **Do not invent a new top-level routing structure for prototypes** — that's the prototype tail wagging the production dog.

## When to use `cva()` vs separate variant components

shadcn-style codebases often use `cva()` (`class-variance-authority`) to encode variants of a single component. For prototypes:

**Use separate components** (`<SingleVariant />`, `<WizardVariant />`, `<HybridVariant />`) when:

- The variants differ in **structure** — different component trees, different flows, different state machines
- Each variant might be the seed of a real implementation later — having them in separate files makes it easy to delete the losers and promote the winner

**Use `cva()` within one component** when:

- The variants differ only in **styling** — density, hierarchy, spacing
- The structure is shared and you're A/B/C-testing visual treatment

For the vast majority of prototypes that get to this skill, the answer is **separate components** — if the differences were only styling, the question probably wasn't worth a prototype.

## Scratch persistence — when the prototype needs data

The protocol's rule 3 says "no persistence by default. State lives in memory." For shadcn prototypes that need *some* data:

- **Hard-coded fixtures** in a `fixtures.ts` next to the prototype. 90% of cases.
- **In-memory store via Zustand or Jotai** with no persistence middleware. Resets on every reload — exactly what you want for the "is the flow right" question.
- **`localStorage` only if the question is specifically about persistence** (e.g., "should this remember the user's draft between sessions?"). Name the key with a clear `PROTOTYPE_` prefix so it's easy to clean up: `localStorage.setItem("PROTOTYPE_checkout_v2_draft", ...)`.
- **A real database — almost never.** If the prototype's question genuinely needs a DB, it's no longer a prototype.

## Three concrete examples

### Example 1: Checkout flow density

User: *"Should this checkout be a single page or a wizard?"*

```
app/(prototypes)/checkout-v2/
├── page.tsx                 ← orchestrator + VariantSwitcher
├── variants/
│   ├── SingleVariant.tsx    ← everything on one screen
│   ├── WizardVariant.tsx    ← step-by-step with next/back
│   └── HybridVariant.tsx    ← single page with collapsibles
├── fixtures.ts              ← hard-coded cart, user, payment methods
└── NOTES.md                 ← question + decision after the prototype
```

Three real components implementing the t

Related in Design