I have helped build several React component libraries, usually at startups where the first version had to ship quickly. The problems rarely appeared in that first version. They arrived after a component had accumulated enough props to describe several different components at once.

A button might start with variant and size. Then it gains leftIcon, rightIcon, loadingText, fullWidth, and a special layout needed by one screen. Each prop looks harmless. The combinations are the problem.

My default is to compose a component once it owns both behavior and child structure. The common case can keep a simple wrapper, but consumers should be able to drop down to the lower-level parts instead of adding structural props.

Keep the common case simple

Props work well when a component supports a small set of intentional choices:

<Button variant="primary" size="small">
    Save
</Button>

variant and size describe combinations the design system owns and tests. A precomposed Button should cover this standard case, so a consumer does not have to assemble it from five pieces.

The API becomes harder to maintain when props start describing arbitrary structure:

<Button
    text="Save"
    leftIcon={CheckIcon}
    iconPosition="above"
    loadingText="Saving"
/>

iconPosition="above" is a warning that the component now owns a second layout. The next request may need two lines of text or a badge beside the icon. Adding another prop keeps the simple call site intact, but moves every special case into the button.

Let structure be composed

When structure needs to change, expose the stable behavior and let children provide the layout:

<Button.Root variant="primary">
    <Button.Icon>
        <CheckIcon />
    </Button.Icon>
    <Button.Label>Save</Button.Label>
</Button.Root>

The parts can share state through context. Button.Root owns focus, disabled behavior, loading state, and the accessible name. Button.Icon and Button.Label own their small pieces of rendering.

That lower-level API sits beside the precomposed Button used by most screens. When a product needs a different arrangement, consumers can drop down a level instead of adding another structural prop to the shared component. The smaller parts also keep their tests and behavior when the layout changes.

Put values in tokens

Composition does not help if every part invents its own spacing and color. Shared values still need a home.

I use tokens for spacing, type, color, and motion. Every value enters the token system, even if only one component uses it today.

:root {
    --space-control-inline: 0.75rem;
    --space-control-block: 0.5rem;
    --color-control-bg: #111;
    --color-control-fg: #fff;
}

A semantic name such as --space-control-inline explains why the value is shared. A token named --space-3 only says where the number sits on a scale. Both can be useful, but they solve different problems.

Every spacing, color, and typography value should come from the token system, including values that currently appear in one component. If no token fits, I would rather decide whether the system needs a new one than hide an unexplained number in local CSS. That rule is strict on purpose. It prevents one-off values from becoming five nearly identical grays or spacing values later.

Keep base components narrow

Small layout and text components can remove repeated styling, but they need a clear boundary. A Stack that controls direction and gap is easy to predict:

<Stack direction="column" gap="4">
    <Heading>Account</Heading>
    <Text>Update your profile and security settings.</Text>
</Stack>

A Box with props for every CSS property is harder to justify. It often recreates CSS through a larger API and makes responsive behavior difficult to inspect.

The base components I keep are the ones that encode a repeated constraint:

  • Stack applies the spacing scale between children.
  • Text maps supported type styles to semantic elements.
  • VisuallyHidden provides one tested accessibility behavior.

If a wrapper does not enforce a decision or remove repeated behavior, plain HTML and CSS are usually clearer.

Separate behavior from appearance

Interactive components need more than composition. A popover must manage focus, dismissal, keyboard input, positioning, and ARIA attributes. Rebuilding that behavior inside every visual variation creates bugs that are difficult to spot in review.

I prefer a tested behavior layer, such as Base UI, underneath the design-system components. The behavior layer handles the interaction contract. Our components apply tokens, layout, and product-specific defaults.

That separation also makes the escape hatch clearer. A product team can compose a different visual arrangement without replacing keyboard support or focus management.

Use the smallest API that supports the product

There is no prize for the most composable component. A compound API has costs:

  • More imports and markup at each call site.
  • More documentation for valid combinations.
  • More ways for consumers to assemble something the design did not intend.

There is also no prize for the component with the fewest parts. A single component with twenty interacting props hides complexity rather than removing it.

I keep the common component simple and expose lower-level pieces for consumers that need control over structure. Props handle finite visual choices. Composition handles structural changes. If one screen needs an exception, it can drop down a level without expanding the shared API.

That approach has prevented more maintenance work than any universal rule about tokens, base components, or compound APIs. The component library stays useful because it follows the product cases we have, while leaving a deliberate path for the next case that is genuinely different.