Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Scaffold temporary multi-layout UI prototypes with persisted variant switching and a floating preview toggle. Use when comparing 2–5 design options for a component, the user asks to try alternatives before picking one, or when building A/B-style layout previews in React apps.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 48% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 89% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 61% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 66% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 139% | 0% |
Temporary workflow for comparing visual/layout options in the running app, then collapsing to a single implementation when the user picks a winner.
Stack: Any React setup (Vite SPA, Next.js, Remix, etc.). Examples below use shadcn/ui and Tailwind for the toggle UI — swap for the app's own component library and styling — and whatever state management the app already uses (Jotai, Zustand, Redux, React Context — see Preview state).
Everything below is client-side code. If the app uses React Server Components (e.g. Next.js App Router), add 'use client' at the top of each preview file; in other setups, omit it.
Do not use for: API design, routing, or long-lived feature flags.
Colocate under the owning feature folder (route _components, app-components, etc.):
feature-name/
├── feature-name.tsx # router: reads variant state, renders active variant
├── feature-name-data.ts # shared data hook / pure mappers (no layout)
├── feature-name-preview.state.ts # variant union + persisted state + labels
├── feature-name-preview-toggle.tsx # floating switcher (dev-only)
├── feature-name-shared.tsx # optional: icons, value formatting, a11y helpers
└── variants/
├── feature-name-variant-a.tsx
├── feature-name-variant-b.tsx
└── ...Use explicit entry files (feature-name.tsx), not index.ts barrels.
feature-name-data.ts.FeatureItem[] with { id, label, value, isEmpty }.ts// feature-name-data.ts export type FeatureItem = { key: string; label: string; value: string; isEmpty: boolean }; export function useFeatureItems(): FeatureItem[] { // context, hooks, formatters }
variants/.FeatureNameVariantA({ items }: { items: FeatureItem[] }).feature-name-shared.tsx when variants differ only in wrapper/layout (icons, tooltips, empty states).feature-name-preview.state.ts)The only requirement: the chosen variant must persist (survive a reload/navigation) so you can compare options while browsing the real app. Any state approach works as long as it reads/writes a small string union and persists it to localStorage or equivalent. Pick whatever the project already uses:
atomWithStorage — one line, persistence included.persist middleware.localStorage-syncing subscriber, or redux-persist.useState + localStorage. A bare useState hook is not enough: the toggle and the router are separate components, so each would get its own copy and switching wouldn't update the feature live — the state must be shared through a provider (or a store).Whatever you pick, expose the same three things: the variant union/type, a read+write accessor, and a labels map for the toggle UI.
Example implementation (Jotai) — swap for your library of choice:
tsimport { atomWithStorage } from 'jotai/utils'; export const FEATURE_PREVIEW_VARIANTS = ['a', 'b', 'c'] as const; export type FeaturePreviewVariant = (typeof FEATURE_PREVIEW_VARIANTS)[number]; export const featurePreviewVariantAtom = atomWithStorage<FeaturePreviewVariant>( 'my-app:dashboard-feature-preview-variant-v1', // bump suffix when union changes 'a', ); export const FEATURE_PREVIEW_LABELS: Record<FeaturePreviewVariant, string> = { a: 'Option A', b: 'Option B', c: 'Option C', };
Example implementation (plain React, no library) — the file becomes feature-name-preview.state.tsx since it renders a provider:
tsximport { createContext, useContext, useEffect, useState, type ReactNode } from 'react'; export const FEATURE_PREVIEW_VARIANTS = ['a', 'b', 'c'] as const; export type FeaturePreviewVariant = (typeof FEATURE_PREVIEW_VARIANTS)[number]; const STORAGE_KEY = 'my-app:dashboard-feature-preview-variant-v1'; type PreviewContextValue = readonly [FeaturePreviewVariant, (v: FeaturePreviewVariant) => void]; const FeaturePreviewContext = createContext<PreviewContextValue | null>(null); export function FeaturePreviewProvider({ children }: { children: ReactNode }) { // Start from the default and read localStorage after mount — under SSR // (Next.js, Remix, ...) localStorage doesn't exist at render time, and a // render-time read would mismatch on hydration. Harmless in a pure SPA. const [variant, setVariant] = useState<FeaturePreviewVariant>('a'); useEffect(() => { const stored = localStorage.getItem(STORAGE_KEY) as FeaturePreviewVariant | null; if (stored && FEATURE_PREVIEW_VARIANTS.includes(stored)) setVariant(stored); }, []); const setAndPersist = (v: FeaturePreviewVariant) => { setVariant(v); localStorage.setItem(STORAGE_KEY, v); }; return ( <FeaturePreviewContext.Provider value={[variant, setAndPersist] as const}> {children} </FeaturePreviewContext.Provider> ); } export function useFeaturePreviewVariant(): PreviewContextValue { const value = useContext(FeaturePreviewContext); if (!value) throw new Error('useFeaturePreviewVariant requires FeaturePreviewProvider'); return value; } export const FEATURE_PREVIEW_LABELS: Record<FeaturePreviewVariant, string> = { a: 'Option A', b: 'Option B', c: 'Option C', };
{project-or-app}:{area}-{feature}-preview-variant-v{N}.v{N} if you add/remove/rename variants (avoids stale localStorage).feature-name.tsx)tsximport type { ComponentType } from 'react'; import { useFeatureItems } from './feature-name-data'; // Jotai example — replace with your state hook of choice: import { useAtomValue } from 'jotai'; import { featurePreviewVariantAtom, type FeaturePreviewVariant } from './feature-name-preview.state'; import { FeatureNameVariantA } from './variants/feature-name-variant-a'; // ... const variantComponents: Record< FeaturePreviewVariant, ComponentType<{ items: ReturnType<typeof useFeatureItems> }> > = { a: FeatureNameVariantA, b: FeatureNameVariantB, c: FeatureNameVariantC, }; export function FeatureName() { const variant = useAtomValue(featurePreviewVariantAtom); const items = useFeatureItems(); const Variant = variantComponents[variant]; return <Variant items={items} />; }
feature-name-preview-toggle.tsx)Mount once on the page/layout, inside whatever provider your state approach needs (Jotai Provider, Zustand doesn't need one, Context needs its own provider, etc.)—sibling to the feature component.
tsximport { useAtom } from 'jotai'; // swap for your state hook import { Button } from '@/components/ui/button'; // shadcn/ui — swap for the app's button import { cn } from '@/lib/utils'; import { FEATURE_PREVIEW_LABELS, FEATURE_PREVIEW_VARIANTS, featurePreviewVariantAtom, } from './feature-name-preview.state'; // Static class names — Tailwind can't generate CSS for `grid-cols-${n}` template literals. const GRID_COLS: Record<number, string> = { 2: 'grid-cols-2', 3: 'grid-cols-3', 4: 'grid-cols-4', 5: 'grid-cols-5', }; export function FeatureNamePreviewToggle() { const [variant, setVariant] = useAtom(featurePreviewVariantAtom); return ( <div className="border-border bg-card/95 fixed right-4 bottom-4 z-50 flex max-w-[min(100vw-2rem,24rem)] flex-col gap-2 rounded-2xl border p-3 shadow-lg backdrop-blur-sm" role="region" aria-label="Layout preview" > <p className="text-muted-foreground text-xs font-medium">Layout preview</p> <div className={cn('grid gap-1', GRID_COLS[FEATURE_PREVIEW_VARIANTS.length] ?? 'grid-cols-3')}> {FEATURE_PREVIEW_VARIANTS.map((option) => ( <Button key={option} type="button" size="sm" variant={variant === option ? 'default' : 'outline'} className="h-8 text-xs" aria-pressed={variant === option} onClick={() => setVariant(option)} > {FEATURE_PREVIEW_LABELS[option]} </Button> ))} </div> </div> ); }
For 4–5 options with long labels, flex flex-wrap gap-1 is a good alternative to the grid.
tsximport { Provider } from 'jotai'; // only needed if your state approach requires a provider export function SomePage() { return ( <Provider> <FeatureName /> {/* other content */} <FeatureNamePreviewToggle /> </Provider> ); }
If the page already has a provider for other state, reuse it—do not nest providers unless isolating state.
tabIndex={0} and a descriptive aria-label on the wrapper.role="group", aria-label on the stats region, sr-only labels where needed).When the user picks a variant:
feature-name.tsx (or keep one variants/ file only if large).feature-name-data.ts if it still separates data from UI.feature-name-preview.state.ts, feature-name-preview-toggle.tsx, unused variants/*, feature-name-shared.tsx if no longer needed.feature-name.tsx + data file remain (match repo colocation conventions).PreviewToggle, preview-variant).Do not leave persisted preview state or floating toggles in production unless requested.
Scaffold
Ship
Other measured skills in the registry, with their headline benchmark lift.