From harness-claude
Decomposes reusable UI components into slots, variants, states, sizes to guide API design (props, slots, events), composition vs configuration, compound components, and split/merge decisions.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Anatomy of reusable components covering slots, variants, states, sizes, composition vs configuration, compound components, and when to split vs merge.
Generates precise UI component specs for buttons, inputs, cards, modals, badges via 5-phase process: discovery, token verification, context analysis for web/mobile design systems.
Guides building accessible, composable UI components including ARIA, keyboard navigation, slots, polymorphism, design tokens, npm publishing, and documentation.
Designs reusable UI components in React, Vue, Svelte using composition patterns, CSS-in-JS like Tailwind/styled-components, and API best practices for libraries and design systems.
Share bugs, ideas, or general feedback.
Anatomy of reusable components covering slots, variants, states, sizes, composition vs configuration, compound components, and when to split vs merge.
Every reusable component has an anatomy: the structural parts that make it a coherent, predictable unit. Understanding component anatomy means being able to decompose any component into its slots, variants, states, and sizes -- and knowing which of those should be exposed as API surface vs kept as internal implementation.
The anatomy decomposition procedure:
A slot is a named content area within a component. Slots determine the component's layout structure and define what content consumers can inject.
Named slot example -- Shopify Polaris Card:
<Card>
<Card.Header title="Orders" actions={[{ content: 'Export' }]} />
<Card.Section>
<DataTable rows={orders} columns={columns} />
</Card.Section>
<Card.Section title="Notes" subdued>
<TextContainer>Customer notes appear here.</TextContainer>
</Card.Section>
<Card.Footer>
<Pagination hasNext onNext={handleNext} />
</Card.Footer>
</Card>
Polaris's Card has four slot positions: Header, Section (repeatable), and Footer. Each is a sub-component that enforces its own internal anatomy while participating in the parent's layout.
Slot optionality rules:
Dialog requires a content body.Card works without a footer.Card.Section in Polaris is repeatable; Card.Header is not.GitHub Primer's ActionList uses a repeatable item slot with optional sub-slots: ActionList.LeadingVisual, ActionList.Description, ActionList.TrailingVisual. Each item's anatomy is: leading visual (optional) + label (required) + description (optional) + trailing visual (optional).
Variants represent distinct visual or behavioral modes. A component can only be in one variant at a time. Variants are typically expressed as a single prop with enumerated values.
Material Design 3 Button variants:
| Variant | Use Case | Visual Treatment |
|---|---|---|
| Filled | Primary action, high emphasis | Solid background, md.sys.color.primary |
| Outlined | Secondary action, medium emphasis | 1px border, transparent background |
| Text | Tertiary action, low emphasis | No background, no border |
| Elevated | Primary action needing lift | Solid background + md.sys.elevation.level1 |
| Tonal | Secondary action with subtle fill | md.sys.color.secondaryContainer background |
Each variant defines its own token mapping for background, text color, border, and elevation. The variant prop selects which mapping applies.
Variant decision criteria:
Button and an IconButton share variants (primary, secondary) but differ in slots (text+icon vs icon-only). Material Design 3 treats them as separate components. This is correct: the slot difference changes the component's anatomy.States represent conditions that change a component's appearance or behavior without changing its identity. Unlike variants, states can combine.
Complete state matrix for an interactive component:
| State | Trigger | Visual Change | Combines With |
|---|---|---|---|
| Default | None | Base appearance | -- |
| Hover | Mouse enter | Background lightens/darkens | Focus |
| Active | Mouse down | Background darkens further | Focus |
| Focus | Keyboard tab | Focus ring (2px, offset 2px, brand color) | Hover |
| Disabled | disabled prop | 38% opacity, no pointer events | -- (exclusive) |
| Loading | loading prop | Spinner replaces content, no pointer events | -- (exclusive) |
| Selected | selected prop | Check mark, filled background | Hover, Focus |
| Error | error prop | Red border, error icon | Hover, Focus |
State combination rules:
Salesforce Lightning defines state tokens per component: --slds-c-button-color-background-hover, --slds-c-button-color-background-active. Each state has its own token rather than relying on computed modifications (darken by 10%), making states deterministic and themeable.
Components typically support 3-5 sizes that align with the system's vertical rhythm and density. Each size affects height, padding, font size, and icon size proportionally.
Atlassian Design System button sizes:
| Size | Height | Horizontal Padding | Font Size | Icon Size |
|---|---|---|---|---|
| compact | 24px | 4px | 12px/16px lh | 16px |
| default | 32px | 8px | 14px/20px lh | 20px |
| large | 40px | 12px | 14px/20px lh | 24px |
Size should scale all related dimensions proportionally. A common mistake is scaling only height while leaving padding and font size fixed, producing visually unbalanced components.
When to support custom sizes: Almost never. If your system needs a 36px button for one specific context, the answer is usually a new size tier (md-large) or re-evaluating the layout, not a height prop. Escape hatches like style={{height: 36}} break system consistency.
The fundamental API design decision: does a component accept its content as props (configuration) or as children/sub-components (composition)?
Configuration model:
<Select
label="Country"
options={[
{ value: 'us', label: 'United States' },
{ value: 'ca', label: 'Canada' },
]}
value={selected}
onChange={setSelected}
/>
Composition model:
<Select label="Country" value={selected} onChange={setSelected}>
<Select.Option value="us">United States</Select.Option>
<Select.Option value="ca">Canada</Select.Option>
<Select.Separator />
<Select.Option value="other" disabled>
Other
</Select.Option>
</Select>
Decision procedure:
| Criterion | Prefer Configuration | Prefer Composition |
|---|---|---|
| Content is uniform data | Yes | -- |
| Content includes mixed types | -- | Yes |
| Ordering matters semantically | -- | Yes |
| Consumer needs conditional rendering | -- | Yes |
| Simple cases dominate (>80%) | Yes | -- |
| Slots have custom rendering needs | -- | Yes |
Shopify Polaris migrated ResourceList from configuration (items prop with render callback) to composition (ResourceList.Item children) in v12 because consumers needed conditional items, dividers, and custom sub-groups that the configuration model could not express cleanly.
Compound components are a set of components that work together to form a complete UI pattern, sharing implicit state through context.
GitHub Primer ActionMenu as compound component:
<ActionMenu>
<ActionMenu.Button>Menu</ActionMenu.Button>
<ActionMenu.Overlay>
<ActionList>
<ActionList.Item onSelect={handleEdit}>Edit</ActionList.Item>
<ActionList.Item onSelect={handleDelete} variant="danger">
Delete
</ActionList.Item>
</ActionList>
</ActionMenu.Overlay>
</ActionMenu>
ActionMenu, ActionMenu.Button, and ActionMenu.Overlay share open/close state through React context. The consumer never manages isOpen manually.
When to use compound components:
Tab without TabList is nothing)Split a component when:
icon and avatar are never used together -- make IconButton and AvatarButton)isInline that completely reorganizes layout indicates two components)Merge components when:
Prop sprawl. A component with 25+ props that tries to handle every possible configuration. Material UI's TextField at one point had 30+ props because it merged input, label, helper text, adornments, and validation into one component. The fix: decompose into compound components (Input, InputLabel, FormHelperText) that compose together.
Boolean prop explosion. Using boolean props for what should be a variant enum: isPrimary, isSecondary, isGhost, isDanger. This allows invalid states (isPrimary && isSecondary). Use variant: 'primary' | 'secondary' | 'ghost' | 'danger' instead. Shopify Polaris enforces this with TypeScript discriminated unions.
God component. A single component that renders differently based on a type prop, where each type has a completely different slot structure. If <Card type="product"> and <Card type="user"> have different slots, different states, and different sizes, they are not the same component. They share a name but not an anatomy. Split them.
Implicit slot ordering. A component that changes behavior based on the order of children without documenting or enforcing that order. If <Toolbar> renders differently when <ToolbarSearch> is the first child vs the last, that is implicit coupling. Either enforce order with named slots or make order irrelevant.
Shopify Polaris Button has a clean anatomy: variant (5 options), size (3 tiers: slim/medium/large with 28px/36px/44px heights), tone (critical for destructive variant), icon (optional leading/trailing), loading, disabled. This is 7 props covering 4 anatomical dimensions. They avoid prop sprawl by keeping icon-only buttons as a separate IconButton component.
GitHub Primer Dialog uses compound components: Dialog, Dialog.Header, Dialog.Body, Dialog.Footer, Dialog.CloseButton. The header slot is optional (some dialogs are headerless alerts). The footer slot supports left-aligned and right-aligned action groups. State (open/close) flows through context. Width is a variant: small (296px), medium (480px), large (640px), xlarge (960px).
Salesforce Lightning DataTable is the canonical complex organism. Its anatomy includes column definitions (with sort, resize, wrap), row selection (single/multi), inline editing, infinite scroll, and column reordering. Rather than 40+ props, it uses composition: <DataTable> wraps <Column> children, each column defines its own rendering, sorting, and editing behavior.
Robin Rendle, "Component API Design" (2021). React documentation on Composition vs Inheritance. Radix UI Primitives documentation on compound component patterns. Shopify Polaris component architecture documentation.