mcpField
C
components

components

github.com/paragonhqDesign & UI044 installsUpdated 5w ago
Repository

About components

components is a design skill for building consistent, accessible interface components. It guides component APIs, interaction states, styling, and testing, with headless primitives for complex behavior.

Key features

  1. 01Choose primitives for complex interactions
  2. 02Define complete interaction states
  3. 03Compose controlled and uncontrolled APIs
  4. 04Style with design tokens
  5. 05Test keyboard, responsive, and assistive use

Use cases

  1. 01Plan an accessible dialog or menu
  2. 02Review component state coverage
  3. 03Standardize component APIs and styling

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/paragonhq/stealth-skills --skill components

After installing, type/componentsin Claude Code to confirm it loaded.

Details

From the project README

components

A component is a contract: it looks the same everywhere, behaves the same everywhere, and does not need to be looked at to be trusted. Most generated components fail the third part.

Primitive or hand-rolled?

Hand-roll only what has no behaviour: a card, a badge, a divider, a stat. Anything with focus management, keyboard interaction, positioning or ARIA uses a headless primitive.

NeedPrimitiveWhy not hand-roll
Dialog, sheet, popover, menu, select, tooltip, tabs, toggle, slider, comboboxBase UI, Radix, React Aria, Arkfocus trap, escape, outside click, typeahead, ARIA, portal, collision
Drawer with dragVaulgesture physics, snap points, scroll locking
ToastSonnerstacking, swipe, timers, a11y announcements
Command palettecmdkfiltering, keyboard, groups, a11y
Animated numbersNumberFlowdigit columns, locale formatting
Data tableTanStack Table (headless)sorting, virtualisation, column state
Formsnative + a small validation libconstraint API, focus on first error

The rule: if it has a spec in ARIA Authoring Practices, use a primitive that implements it.

The state matrix

Every interactive component covers all of these before it is done. Write them down; check them off.

StateVisualBehaviour
Default——
Hover (hover devices only)one-step change—
Focus-visiblering—
Active / pressedscale .97 or darken—
Selected / checked / onfill or accentaria-* reflects it
Disabled50% opacity, no hovernot focusable, aria-disabled if it must stay in the tab order
Busy / loadingspinner in place, same widthignores input, aria-busy
Error / invalidborder + messagearia-invalid, aria-describedby to message
Empty (for containers)one line + one action—
Read-onlytext styling, no affordance—

Sizes: two or three (sm, md, lg). Variants: primary, secondary (border), ghost, danger. Not seven. Not outline-gradient-xl.

Style from state, not from props

Style reads the element's actual state so the component and its CSS cannot disagree:

<button data-state={open ? "open" : "closed"} data-size={size} data-variant={variant} aria-busy={busy || undefined} />
.button[data-variant="primary"] { ... }
.button[data-state="open"] { ... }
.button[aria-busy] { ... }
.button:where(:disabled, [aria-disabled="true"]) { ... }

Primitives already emit data-state, data-highlighted, data-disabled; style those rather than mirroring them into React state.

API by composition

Small parts that compose beat one component with forty props:

<Dialog.Root>
  <Dialog.Trigger asChild><Button>Rename</Button></Dialog.Trigger>
  <Dialog.Content>
    <Dialog.Title>Rename file</Dialog.Title>
    <Dialog.Description>…</Dialog.Description>
    <Dialog.Close asChild><Button variant="secondary">Cancel</Button></Dialog.Close>
  </Dialog.Content>
</Dialog.Root>

Guidelines:

  • asChild / render to put behaviour on the user's element instead of wrapping.
  • Spread rest props onto the root element; forward className and style; forward refs.
  • Boolean props for states, string unions for variants, no type="..." collisions with native attributes.
  • Controlled and uncontrolled both work: value + onValueChange, or defaultValue. Never half of one.
  • Event handlers are called onX and receive the native event or a plain value, never a synthetic object of your own invention.
  • No isLoading/loading duplicates; one name, used everywhere.

Tokens, not values

A component references tokens for colour, radius, spacing, easing and duration. If a component has a hex, a 0.3s or a cubic-bezier inline, it has forked the system.

Radius is consistent per level: inputs and buttons 6–8px, cards and popovers 8–12px, dialogs 12–16px, pills full. Nested radii shrink: an element inside a 12px card uses 8px, not 12px.

Positioning layers

Popovers, menus and tooltips: portal to body, position from the trigger with collision handling, transform-origin set from the resolved placement, and a scale/opacity entrance of ~160ms. Dialogs: centred, backdrop, scroll lock that does not shift layout (compensate for the scrollbar), focus moved in and returned on close.

Z-index is a scale, not a number: --z-sticky: 10; --z-dropdown: 20; --z-dialog: 30; --z-toast: 40. Anything above 50 is a bug.

Inputs

  • A visible label. Placeholders are hints, not labels.
  • Error message below, linked with aria-describedby, appears after blur or submit, not on every keystroke.
  • Height matches the button height in the same size (32px sm, 36px md, 40px lg).
  • 16px font size on touch, or the browser zooms.
  • inputmode and autocomplete set. enterkeyhint on phones.
  • Icons inside inputs are decorative unless they are buttons; buttons have a 32px hit area even if they draw at 16px.

Lists and rows

  • The whole row is the target (an ::after pseudo-element covering the row on the link), not just the name.
  • One hover highlight that follows the pointer, not per-row backgrounds.
  • Keyboard: j/k or arrows move, Enter activates, and the focused row is visibly outlined.
  • Truncate the description, never the name.
  • Virtualise past ~200 rows.

Icons

One set. One stroke width. One size in a context (14px in dense UI, 16px in comfortable, 20px in touch). currentColor so they follow text. Decorative icons are aria-hidden; icon-only buttons have aria-label.

Tables

  • Header row sticky, with a bottom border.
  • Numbers right-aligned and tabular; text left; never centred.
  • Row height 32–40px; zebra striping only in very dense tables, and at the --hover level.
  • Sort indicator in the header, with the active column visible without hovering.
  • Empty state inside the table region, not replacing it.

Testing a component

Before calling it done, open it and:

  1. Tab through it with the mouse unplugged.
  2. Press it, hold it, drag off it, release.
  3. Resize to 375px.
  4. Toggle dark and light.
  5. Turn on reduced motion.
  6. Fill it with the longest realistic content and the shortest.
  7. Screen reader: does it have a name, a role and a state?

Marks of a generated component

SignFix
Custom dropdown with useState and an outside-click hookMenu primitive
variant with eight valuesfour
className not forwardedforward it, merge it
Inline transition: all 0.3stokens
Spinner replaces the label and the button shrinksspinner overlays, width fixed
Icon-only button with no labelaria-label
div onClickbutton
Modal with no focus trapDialog primitive

From the catalogue

Comments

Markdown supported