variant
Answers which of these rather than is this right. Builds several genuinely different versions of one piece of UI behind a picker in the real page, so you can flip between them and promote the one that wins.
Install
npx skills add https://github.com/jakubkrehel/skills --skill variantThe picker
The control that switches variants. It sits beside the thing being judged, so build the spec below and leave it alone.
Deliberately outside the design system
Never style the picker with the project's tokens, fonts or colors. One that looks native to the product becomes part of what you are looking at.
One dark neutral surface, the system font stack and no project variables. It does not follow the theme, because dark reads as chrome over both light and dark pages.
A grouped list, so it scales
Variants sit in a vertical list under the group headings from Name the axis before writing code. A row of tabs runs off the screen past about seven variants; a grouped list stays scannable at thirty.
- Every group has a heading, even when there is only one, and every heading carries the same small arrow icon.
- Variants and groups keep the order they were written down in.
Behavior
- It sets a
__variantsearch param and reads the active variant back from it. The URL is the source of truth, so every variant is a link. - The arrow keys, up and down or left and right, step through every variant in list order, across groups.
Hhides and shows the picker, list and select alike, for screenshots and screen recordings.- Key handling ignores events with a modifier key, events already
defaultPreventedand events from inputs, textareas, selects and contenteditable elements. - The active button carries
aria-pressed="true"and scrolls into view inside the list. The list carries a label, and each group is arole="group"labelled by its heading. - The piece switches instantly and keeps the scroll position. Only the active label eases its font weight.
- The list and the select are both always rendered, and a container query shows one, so the active variant survives a resize.
Structure
Placement and styling
A container query on the page picks the layout:
- Room for the list beside the piece: two columns, the list then the piece at its production width. The list is sticky and scrolls inside itself once it outgrows the viewport, so it stays put while you flip variants.
- No room: the select sits above the piece, in the page flow. A phone opens its native menu for it, which suits a long list.
The page spans the full viewport width, because an app's layout often caps content near the piece's own width. The query measures the page inside its padding, so the breakpoint is the list and the gap plus the piece, which is 232px plus the production width. The CSS below uses a 644px piece. Put the production width in max-width and the grid column, and set the query to 232px plus it.
The list's 6px padding keeps its own scrolling from clipping focus rings. The reset strips the select's native arrow, so the chevron is what shows it opens.
all: unset keeps the project's global button, nav and select styles out. In a framework, keep the class names and the structure and change only the rendering syntax.