Give each component a clear responsibility
Separate reusable interaction, product behavior, and page composition; design APIs around valid states.
Why this matters
Say you've pasted the same project card onto three pages, and now every fix has to happen three times. A good component ends that loop: it makes a recurring decision easy to express correctly. Draw its boundary around behavior and ownership, not line count or naming rules.
What to understand
Separate three kinds of responsibility: primitives own reusable semantics and interaction, features own product vocabulary and workflow, layouts and pages own regions, ordering, and route context. A project status badge used in three places still belongs with project behavior, not with the generic Badge it reuses.
Before you abstract, write down the contract — what it takes in, what it sends back, which states it can be in, and who owns each value. Let people slot content in to combine things; reach for variants when one clear meaning changes the look.
Watch for
- Boundaries drawn around size or naming instead of behavior and ownership.
- A generic primitive learning every product status.
- Booleans that allow nonsense combos — destructive, success, compact, expanded, loading all at once.
- The same Tab/Enter/Space/Escape treatment on every control.
- Important content vanishing into "..." to keep rows equal height.
- Renaming a prop or changing a default without listing every consumer.
Strong default
Ask your agent: "list every state this component can be in, and who owns each one." Then ask: "can these props contradict each other? Replace them with one variant." Keep a one-off composition local until a second real use shows up.
When this doesn't apply
A plain status label needs no hover state. An uncontrolled component keeping its own value is fine until a parent must own it — support both only when you truly need both. Only build a new abstraction when the payoff beats maintaining it and moving people over.
In practice
| Layer | Owns | Example |
|---|---|---|
| Primitive | Reusable semantics, interaction, and visual conventions. | Button, Dialog, Badge. |
| Feature | Product vocabulary, state, and workflow. | ProjectStatus, InviteMember, RequestList. |
| Layout or page | Regions, ordering, data composition, and route context. | WorkspaceLayout, ProjectPage. |
type ProjectStatusProps = {
status: 'active' | 'paused' | 'completed' | 'archived';
};This one turns a status into a readable label and look. Anything unexpected from the server — saved data that survives reload — must be caught at the validated data boundary or given an intentional fallback. A TypeScript union alone can't check a server response.
For anything interactive, write down keyboard behavior, the accessible name, where focus lands and returns, how it closes, what shows while it's working, and how you recover. For every component, decide how it handles growing content and supported themes.
Verify
Every consumer listed before a contract change, with what visible behavior must stay the same. Every state owned, every contradiction removed, growing content and themes checked in place.
Related skills
Use $component to create or revise a contract. Build it with component development, then judge the real composition with interface craft.
Last updated on