Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Design a UI component spec to the house quality bar — anatomy, variants, sizes, the 8 states, token mapping, and accessibility. Use when the user wants to design or document a component (button, input, tabs, toast, combobox, date picker, modal, etc.) at the spec level before or alongside code. For generating framework code, use design-code.
.claude/skills/plugin87-design-component/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-04 | ✗→✓ | ▲ Improved | 75% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 49% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 54% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 57% | 0% |
| case-18 | ✗→✓ | ▲ Improved | 51% | 0% |
Produce a complete component specification matching the project format.
CLAUDE.md → "Component Guidelines" (quality bar, the 8-state table) and "Atomic Design".components/atoms.md, molecules.md, organisms.md, templates.md, navigation.md, feedback.md, forms-advanced.md, overlays.md. Match the existing spec format.accessibility/aria-patterns.md and contrast/target rules from accessibility/wcag-checklist.md.tokens/*.json) — sizes via sizing.json, states via states.json.taste/design-taste.md (states, focus, no slop).python3 scripts/scaffold_component.py "<Name>" to emit a stub, then fill it in.Spec with: anatomy diagram, variants table, sizes table, all 8 applicable states, token mapping, accessibility (role/keyboard/SR), and a note to render via frameworks/adapter-protocol.md.
A component is only "correct" when every variant × state renders right — not just the resting default. Build a states harness: render the component in each applicable state (default, hover, focus, active, disabled, loading aria-busy, error aria-invalid, selected aria-pressed/aria-selected) × each variant in one HTML file (see examples/component-states/button.html). Then RUN the gates and report their real output (CLAUDE.md → Verification Protocol):
node scripts/verify_states.mjs <harness> [--dark] — contrast of every element in default/hover/focusnode scripts/axe_audit.mjs <harness> [--dark] — ARIA/role/name/label correctnessnode scripts/measure_render.mjs <harness> [--dark] — every text element AAnode scripts/verify_focustrap.mjs <harness> --open=<trigger>Every state must pass in light AND dark before the component is "done". Never claim a state is correct without a gate proving it.
The contrast/axe gates pass while the UI is still visibly broken: a checkbox that doesn't toggle, a dash sitting at the bottom of its box, a checkmark and an indeterminate dash with mismatched stroke weight, a control that's too heavy. You must screenshot the harness and inspect it before claiming done — for every state, and after interaction. Playwright + system Chrome:
jsconst b = await chromium.launch({channel:'chrome'}); const p = await b.newPage({deviceScaleFactor:4}); await p.goto('file://'+abs); await p.addStyleTag({content:'*{transition:none!important}'}); await p.mouse.move(2000,2000); // park pointer OFF the component await p.locator('.stack').first().screenshot({path:'/tmp/x.png'});
Read the PNG. Then look for, specifically:
await loc.click(); expect(await loc.isChecked())). A custom control whose overlay box covers the real <input> will not toggle unless the box has pointer-events:none (or an enclosing <label> forwards the click).display:grid box holds an opacity:0 sibling plus a ::after, the pseudo lands in row 2 → use display:none on the hidden sibling, or one container child.<svg>, two <path> toggled by state — same stroke-width), never an svg check vs a CSS ::after rect (they read as different weights).Consistency across files is non-negotiable. The same component (e.g. checkbox) must use byte-identical CSS + markup in every harness/page. A checkbox that looks thin in form-controls and heavy (native accent-color) in data-table is a bug. Factor one pattern, reuse it verbatim.
Real <input> underneath (keeps native a11y + keyboard); a drawn .box overlay with pointer-events:none; check + dash as two <path> in one <svg> toggled by :checked / :indeterminate; 1.5px border-strong, .25rem radius, .62rem glyph, stroke-width:2 round caps. Reference: examples/component-states/form-controls.html and data-table.html (select-all uses indeterminate). Native accent-color renders too heavy — do not use it when the house look is "thin".
Build mobile-first; a fixed-px width that can't shrink is a bug. Run node scripts/verify_responsive.mjs <file|dir> — it loads each harness at 280/320/414px and fails on any horizontal overflow. The four recurring causes and their fixes:
inline-size:Npx → inline-size:100%;max-inline-size:Npx (cap, don't pin).<ul>/<ol> default 40px inline-start padding (a *{margin:0} reset does NOT clear padding) → padding:0;margin:0 on every list. This also silently mis-aligns a list's edge vs a sibling block (looks like unequal widths) — same fix.flex-wrap:wrap, or for tabs overflow-x:auto + .tab{flex:none}.grid minmax(Npx,1fr) min larger than viewport → minmax(min(Npx,100%),1fr).Timing/easing are tokens (--duration-fast|normal|slow, --ease-out|in|in-out|emphasized in the theme; --transition-micro = fast ease-out). Never hardcode ms/curves. A component that toggles open/closed must animate its height, not just rotate a chevron — collapse via hidden/display:none alone reads as "rigid, no transition". Smooth-height pattern (no JS measuring): wrap content in an inner that clips overflow, animate the grid track:
css.panel{display:grid;grid-template-rows:0fr;transition:grid-template-rows var(--duration-normal) var(--ease-emphasized)} .panel.open{grid-template-rows:1fr} .panel > .inner{overflow:hidden;min-block-size:0}
Keep a11y: expand = remove hidden then add .open next frame; collapse = remove .open, set hidden on transitionend. Reference: accordion in examples/component-states/overlays.html. Always honor @media(prefers-reduced-motion:reduce){…transition:none}.
auto-fit, never auto-fill for card grids. auto-fill keeps empty phantom tracks so 3 cards cluster left with a void on the right; auto-fit collapses empties so cards stretch to fill the row. Always repeat(auto-fit,minmax(min(Npx,100%),1fr)).align-items:stretch on the grid, AND make the shorter panel's body fill — .panel{display:flex;flex-direction:column} + the inner region flex:1. A child sized with block-size:% (e.g. chart bars) needs a definite-height ancestor (a flex:1 box or explicit height), or the % resolves to 0 and the element collapses. Wrap the bar in a flex:1 .barbox and give the bar block-size:% of that.flex:1 capped by max-inline-size stops growing and leaves empty space after the last item. Push the right-hand cluster with margin-inline-start:auto on its first element.display:block so opening the sidebar pushes main down. Reference: examples/component-states/app-shell.html.Hand-approximated SVG path data renders as broken glyphs (a help "?" became a dot; settings became a hamburger). Use verbatim lucide paths, referenced by name via an injected <symbol> sprite — examples/component-states/icons.js defines each icon once and <svg class="ico" aria-hidden="true"><use href="#i-NAME"/></svg> uses it. No per-use path duplication, no network, offline + gate-safe. Add a new icon to icons.js once; never paste raw paths into markup. (Inline lucide is acceptable only if the path is copied verbatim from lucide.) .ico{stroke:currentColor;fill:none;stroke-width:2} — color via currentColor.
A no-text control (carousel dot, kebab, icon button) is held to 3:1 (WCAG 1.4.11), not 4.5 — verify_states applies this automatically when an element has no direct text node. Two traps it catches:
<button> keeps the UA color:buttontext (≈black) regardless of theme → set its color to the actual indicator color and drive the visual via currentColor (e.g. dot is a ::before{background:currentColor}), so the gate measures the real thing.--color-chart-N, --color-surface-brand) invert between light/dark; white text or a teal indicator on them passes in one mode and fails the other. Use dark-aware values (override in [data-theme="dark"]) or stable tokens (--color-action-primary, --color-text-link which adapts) for avatars, active dots, and selected states.| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 35,768 | 29,425 | -18% | 1 | 1 | 0% | 6,213 | 8,002 | +29% | 0 | 0 | — |
case-02 | fail→fail | 30,477 | 25,050 | -18% | 1 | 1 | 0% | 6,194 | 7,548 | +22% | 0 | 0 | — |
case-03 | fail→fail | 26,627 | 18,681 | -30% | 1 | 1 | 0% | 4,913 | 6,079 | +24% | 0 | 0 | — |
case-04 | fail→pass | 17,609 | 15,281 | -13% | 1 | 1 | 0% | 3,060 | 5,343 | +75% | 0 | 0 | — |
case-05 | pass→pass | 8,785 | 6,073 | -31% | 1 | 1 | 0% | 1,481 | 3,599 | +143% | 0 | 0 | — |
case-06 | pass→pass | 9,196 | 4,814 | -48% | 1 | 1 | 0% | 1,680 | 3,523 | +110% | 0 | 0 | — |
case-07 | pass→pass | 11,893 | 20,066 | +69% | 1 | 1 | 0% | 1,954 | 6,482 | +232% | 0 | 0 | — |
case-08 | pass→pass | 13,819 | 7,322 | -47% | 1 | 1 | 0% | 2,259 | 3,930 | +74% | 0 | 0 | — |
case-09 | pass→pass | 15,795 | 11,331 | -28% | 1 | 1 | 0% | 2,624 | 4,654 | +77% | 0 | 0 | — |
case-10 | fail→pass | 12,815 | 2,761 | -78% | 1 | 1 | 0% | 2,059 | 3,069 | +49% | 0 | 0 | — |
case-11 | fail→pass | 13,687 | 4,321 | -68% | 1 | 1 | 0% | 2,153 | 3,313 | +54% | 0 | 0 | — |
case-12 | fail→pass | 10,711 | 2,045 | -81% | 1 | 1 | 0% | 1,872 | 2,938 | +57% | 0 | 0 | — |
case-13 | pass→pass | 4,014 | 4,231 | +5% | 1 | 1 | 0% | 614 | 3,209 | +423% | 0 | 0 | — |
case-14 | pass→pass | 4,276 | 4,841 | +13% | 1 | 1 | 0% | 663 | 3,295 | +397% | 0 | 0 | — |
case-15 | pass→pass | 5,519 | 4,614 | -16% | 1 | 1 | 0% | 885 | 3,399 | +284% | 0 | 0 | — |
case-16 | pass→pass | 12,966 | 6,231 | -52% | 1 | 1 | 0% | 2,180 | 3,585 | +64% | 0 | 0 | — |
case-17 | pass→pass | 4,958 | 5,311 | +7% | 1 | 1 | 0% | 835 | 3,404 | +308% | 0 | 0 | — |
case-18 | fail→pass | 16,761 | 9,940 | -41% | 1 | 1 | 0% | 2,900 | 4,369 | +51% | 0 | 0 | — |
case-19 | pass→pass | 16,508 | 14,210 | -14% | 1 | 1 | 0% | 2,699 | 5,196 | +93% | 0 | 0 | — |
case-20 | fail→pass | 6,926 | 2,023 | -71% | 1 | 1 | 0% | 1,139 | 2,854 | +151% | 0 | 0 | — |
case-21 | pass→pass | 17,754 | 21,698 | +22% | 1 | 1 | 0% | 3,331 | 7,096 | +113% | 0 | 0 | — |
case-22 | pass→pass | 12,704 | 12,695 | -0% | 1 | 1 | 0% | 2,325 | 5,103 | +119% | 0 | 0 | — |
case-23 | pass→fail | 15,649 | 6,786 | -57% | 1 | 1 | 0% | 2,783 | 2,933 | +5% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted, and 22 counted toward the lift figure. The other 1 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +22 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.