Component API Design — Engineering Skill
Standards for the surface a reusable component exposes to the code that uses it: which props exist, how variation is expressed, who owns the state, and how the API changes over time. Framework-neutral — the principles hold for React, Vue, and Angular; examples are TSX with the slot equivalent noted.
Builds on.
architecture-and-designfor the design principles underneath (SOLID — especially open/closed, cohesion, dependency direction) andreact/angular/vuefor the framework's actual props, slots, and ref API. On a conflict,architecture-and-designdecides the principle, the framework skill decides the API syntax, and this skill decides the shape of the component's public contract. Load a sibling only when the task turns on its layer; if it is not loaded, apply that layer from general knowledge and do not block.
This SKILL.md is self-sufficient: the Ruleset below is the complete, enforceable list. Each
references/<topic>.md holds that group's reasoning and ❌ / ✅ code, and
references/worked-example.md a full review pass; open them for depth when your runtime allows.
How to Use This Skill
Pick the mode that matches the task. Do the steps in order.
| Mode | Steps |
|---|---|
| Design — design or extend a component's public API | 1. Write the smallest props contract that covers the real use cases; make illegal states unrepresentable (props-contract). 2. Express structural variation with slots, behavioural variation with props (slots-vs-config). 3. For any value the component holds, decide the controlled/uncontrolled contract up front (controlled-uncontrolled). 4. Reach for a compound component only when parts must vary independently and share state (compound-components). 5. Run the Ruleset as a checklist. Fix each fail before you hand off. |
| Review — check a component API in a diff | 1. Run the Ruleset against the diff. 2. Write one finding per fail, in the Output Format below. 3. Order the findings: must-fix first, then consider. 4. If nothing fails, say so in one line. Do not invent findings. |
| Evolve — change a shared component without breaking consumers | 1. Classify the change: additive (safe), behavioural (risky), or breaking (renamed/removed prop, changed default, changed markup contract). 2. For anything past additive, add the new API, deprecate the old with a pointer, and keep both for a release. 3. Ship a codemod for a rename; record it in the changelog with a version. 4. Never change a default value or the DOM/slot contract in a patch release. |
Output Format
Write one finding per line:
<severity> · <topic> · <file>:<line> — <what is wrong>. <the fix as an action>.
<severity>ismust-fix(breaks a rule in this skill, or breaks a consumer) orconsider(safe, but a rule prefers another form).<topic>is a Ruleset topic slug (props-contract,slots-vs-config,controlled-uncontrolled,compound-components,versioning).
Rules for Every Mode
- Name the Ruleset topic when you enforce a rule.
- The public API is a contract with every consumer. Judge a change by what it costs them, not by how clean it looks in the component.
- Prefer removing a prop to adding one. Every prop is forever, is a test case, and is a thing the next reader must understand.
Ruleset
props-contract → references/props-contract.md
- The props type is the minimal set that covers real use cases; a speculative "might need it" prop is not added until a caller needs it.
- Illegal combinations are unrepresentable — a discriminated union over a
variantprop, not four independent booleans that can all be true. - A
variant/size/toneaxis is a string-literal union, not a boolean per value (primary/secondary, notisPrimary+isSecondary). - Props are named for intent, stable, and consistent with the library's vocabulary — one name for one concept across every component.
- A handler follows the framework's event convention (
onChangein React, avalueChangeoutput in Angular, achange/update:valueemit in Vue). - A boolean prop reads as a state (
disabled,loading), not a negated setting (enabled={false}). - A prop's type is a deliberate public type, never the component's private implementation type.
- The props type is exported and documented, and
childrenis typed explicitly. - There is no single
config/optionsobject prop standing in for a real API; sensible defaults mean the common case needs almost no props. - A prop that passes through to a DOM node (
className,aria-*,data-*,id) is forwarded, and the component spreads remaining props onto its root or a named element deliberately.
slots-vs-config → references/slots-vs-config.md
- Structural or content variation is a slot —
children, named slots, or a render prop — not arenderHeader/showFooter/leftIcon/rightIconpile of props. - A slot is used when the caller supplies markup; a prop is used when the component decides the markup from a value.
- A component is open for extension without editing it: a new layout is a new composition of its slots, not a new boolean.
- A render prop or slot that exposes internal state passes a typed, minimal, stable argument object.
- Slot names describe the position or role (
header,actions,empty), not a specific use (searchBoxSlot).
controlled-uncontrolled → references/controlled-uncontrolled.md
- A value the component holds offers both modes:
value+onChangefor controlled,defaultValuefor uncontrolled, and the component never switches an instance between them. -
defaultValueis read once on mount; after that the component owns the value until unmount. - In controlled mode the component renders exactly what
valuesays and callsonChangewith the requested next value — it does not also keep its own copy. - Uncontrolled is the default for a simple input; controlled is required only when the value drives other UI or is validated live.
- Any imperative escape hatch (
refwithfocus()/scrollIntoView()/reset()) is small, named, and documented — not a handle to the internals.
compound-components → references/compound-components.md
- The compound pattern (
<Tabs><Tabs.List><Tabs.Tab/></Tabs.List></Tabs>) is used only when the parts must be arranged or omitted by the caller and share implicit state. - Shared state passes through context (or the framework equivalent), not cloned children or prop-drilling; a subcomponent used outside its parent fails with a clear error.
- For a fixed structure with no caller-controlled arrangement, a flat props API is simpler and is preferred.
- The composed whole is accessible as a unit — roles,
ariawiring, and keyboard interaction span the subcomponents, per the relevant APG pattern (accessibility). - Subcomponents are namespaced on the parent (
Tabs.Tab) or exported together, and each is typed.
versioning → references/versioning.md
- An additive change (a new optional prop, a new slot, a new variant value) is a minor release; a renamed or removed prop, a changed default, or a changed DOM/slot contract is a major.
- A prop being removed or renamed is first deprecated — kept working for one major, marked
@deprecatedwith the replacement named, and warned on in development. - A default value never changes in a patch or minor release; changing it is a breaking change because it alters existing renders.
- A rename ships with a codemod, and every breaking change is in the changelog with the version and the migration.
- The component's rendered DOM structure and class/slot contract are treated as API — consumers style and query against them.
Limits
This skill is the public API of a reusable component. It does not cover:
- Broader architecture — layering, feature boundaries, where the design system sits, state management strategy. That is
architecture-and-design. - The framework's prop / slot / ref mechanics —
defineModel,forwardRefvsrefas prop,input()/output(),@ContentChild. Those arereact/angular/vue. - Visual and interaction design of the component — spacing, motion, the design tokens it consumes (
styling-and-design-tokens). - Accessibility implementation of a widget pattern beyond "wire the composed whole as a unit" — the APG patterns and testing live in
accessibility. - Documentation tooling, Storybook, visual regression, and package publishing mechanics.
- Internal component structure and rendering performance (
web-performance, and the framework skills).
This skill states what the component should expose. It is not a substitute for building the real use cases against the API and feeling where it fights back.
References
This skill composes with:
architecture-and-design— the design principles under these rules (open/closed, cohesion, dependency direction). On a conflict it decides the principle, this skill decides the contract shape.react/angular/vue— the framework mechanics that implement props, slots, controlled pairs, and compound state. On a conflict this skill decides the contract, the framework skill decides the API.accessibility— a compound or slotted component is wired as an accessible unit; the pattern requirements and testing live there.styling-and-design-tokens— the rendered DOM and class contract this skill treats as API is what that skill styles against.