# Component API

> The cross-cutting prop contract every component skill speaks — one shared dialect so ten independently-built components read as authored, not assembled. Fixes the canonical prop vocabulary (variant/size/tone), cva()-based variants that resolve to tokens never literals, asChild polymorphism via Radix Slot (never a custom `as` prop), the cn()/tailwind-merge order that lets a caller's className win, data-slot part targeting and ref-as-prop forwarding for React 19, controlled/uncontrolled parity, and native-attribute + a11y pass-through. Invoke in the Foundation phase after tokens and before the component tier, whenever building or restyling any component (buttons, cards, forms, pricing, data-display, hero…), deciding a prop name, adding a variant, or when the user says "the props are inconsistent", "name this prop", "should this be asChild", "why do the components feel assembled", or a component invents its own variant vocabulary.

- Skill: `blyatiful1/component-api` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add blyatiful1/component-api`
- Raw SKILL.md: https://api.skillmd.com/api/skills/blyatiful1/component-api/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: blyatiful1 (https://skillmd.com/u/blyatiful1)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/blyatiful1/component-api

---


# component-api — one dialect, ten components

**Stage:** Phase 3 — Foundation (after tokens, before the component tier) - **Reads:** design/SYSTEM.md, lib/utils.ts (cn) - **Writes:** no file of its own — the shared prop contract every `components/ui/*` and section component honors (a reference skill, like taste)

## Standard

A site reads as *assembled* when each component is individually fine but they speak ten prop dialects — `variant="primary"` here, `type="filled"` there, `isLarge` next to `size="lg"`, a bespoke `as="a"` beside a real `asChild`. Close inspection catches the seam even when no single component is wrong. First-grade means the opposite: every component is configured the same way, so a developer who has used one has used all of them, and the system reads as one author's work. This is a design-system discipline orthogonal to any component's visual spec — `ultraweb:buttons` owns what a button *looks* like; this skill owns how you *configure* it. It governs; it ships no file. Every component-tier skill obeys it, and `ultraweb:gate-code` greps for its violations.

## The contract

Eight rules. A component may use only the axes it needs, but the ones it uses obey these names and shapes.

1. **The vocabulary is exactly three axes: `variant`, `size`, `tone`.** `variant` = the form/weight the component takes (button: primary/secondary/ghost; card: media/stat/editorial). `size` = the scale step, shared meaning site-wide (`sm`/`md`/`lg`/`xl`). `tone` = semantic intent that selects a token *family* (`default` → foreground/muted, `brand` → primary, `danger` → destructive, `success`/`warning` → status tokens). No synonyms, ever — never `type`, `kind`, `color`, `appearance`, `emphasis`, `intent`, or `scale`. Status-bearing components (badge, alert, toast, input error) express intent through `tone`; a component whose destruction is a *distinct filled treatment* (the Button) may legitimately carry `destructive` as a `variant` — but the name of every axis is fixed.

2. **Variants are `cva()` enums that resolve to tokens, never literals.** Each `variant`/`size`/`tone` value is a row in one `cva()` map whose classes are token-backed utilities (`bg-primary`, `text-sm`, `rounded-md`, `shadow-md`) with a `defaultVariants` for the rest state. A hex, a bare `oklch(`, or an arbitrary `bg-[…]` inside a variant row is a defect — the value you reached for is a token `ultraweb:tokens` already owns. This is where "variants map to the design system" is *enforced*, not merely intended.

3. **`cn()` is the only class merge, and `className` comes last.** `cn` (clsx + tailwind-merge, from `lib/utils.ts`) is the single merge path: `cn(buttonVariants({ variant, size }), className)`. The consumer's `className` is the *final* argument so tailwind-merge lets a caller override a token utility (`rounded-none`, `w-full`) without specificity wars or `!important`. Never string-concatenate classes with `+` or template literals; never put `className` before the variants.

4. **Polymorphism is `asChild` (Radix `Slot`) — never a custom `as` prop.** When a component must render as a different element — a CTA that is a `next/link`, a card that is one big link — it accepts `asChild` and renders `<Slot>`, which merges its classes and props onto the caller's single child. No `as="a"` string, no `component={Link}` prop: those discard the child element's real types and its native props.

5. **Every root carries `data-slot="<component>[-<part>]"`; target parts by data-slot.** The shadcn/React-19 hook for styling, state, and tests. Group styling and state variants select `data-[slot=…]`, `group-data-[state=open]`, `has-[[data-slot=icon]]` — never fragile child combinators (`> div > span`).

6. **Refs are plain props (React 19) — no `forwardRef`; spread `...props` onto the root.** `function Button({ ref, className, ...props })`. Spreading `...props` last-but-one (before `className` is applied via `cn`) forwards every native attribute — `type`, `disabled`, `onClick`, `id`, `name` — *and* every `aria-*`/`role` for free. Native-attribute forwarding and a11y pass-through are the same rule: don't enumerate props you could spread.

7. **Stateful components support controlled *and* uncontrolled, one pattern.** Anything with internal state (tabs, accordion, dialog, switch) accepts `value`/`defaultValue` (or `open`/`defaultOpen`) + `onValueChange`; controlled when the value prop is present, uncontrolled otherwise. This is Radix's own contract — restyle the Radix primitive, never re-implement its state machine as a third mode.

8. **Compound components share state via context; sub-parts are `X.Header`/`X.Body`.** A multi-part component (Card, Field) exports a namespace where each part carries its own `data-slot` and shared config flows through a tiny context — not prop-drilling, not one giant props object. Callers compose the parts; the component owns the wiring.

**Three composition shapes, in order of reach-for:** the **variant enum** (rule 2) is the default closed set and covers ~90% of configuration; **`asChild`/`Slot`** (rule 4) handles element swaps; a **render-prop escape hatch** (`children` as a function receiving state — Base-UI style) is the last resort, only when a caller must inject arbitrary markup a closed variant can't express. Reach left before right.

## Anti-patterns

- `as="a"` / `component={…}` string-polymorphism props — use `asChild` + `Slot` (rule 4).
- `type=`, `kind=`, `appearance=`, `color=`, `emphasis=`, `intent=`, `scale=` as prop names — synonyms of the canonical trio; rename to `variant`/`size`/`tone`.
- Boolean soup — `isPrimary`, `isLarge`, `outlined`, `filled`, `danger` booleans instead of enum axes. Two booleans that can't both be true are one `variant`.
- `forwardRef(` in a new component file — React 19 passes `ref` as a prop; grep for it, treat a hit as a relic.
- `className` merged with `+` or a template literal, or placed *before* the variants in `cn()` — the caller can no longer override.
- A hex, bare `oklch(`, or `bg-[…]`/`text-[…]` inside a `cva()` variant row — defer to a token (`ultraweb:tokens`).
- Child-combinator selectors (`.card > div`) instead of `data-[slot=…]` targeting.
- Re-implementing a Radix stateful primitive's controlled/uncontrolled logic by hand instead of restyling it.
- Per-call-site `className` soup faking a variant that should live in the shared `cva()` map — the single loudest "assembled, not authored" tell.

## Worked example — Tidepool, port-logistics SaaS (Precision Instrument — Neo-grotesque Minimal)

Moved to `references/example.md` — read only when this build's case is genuinely ambiguous; the sections above are the decision material.

## Composes with

Moved to `references/composes.md` — the handoff map; load it when orchestrating this skill against its neighbors.

