The boolean prop problem
We had a Button with 14 boolean props. Every team wanted "just one more variant". The component's prop surface had become its own scope creep, and the component file had become a place where product decisions were quietly encoded.
The tell was the implementation: a 40-line chain of conditionals deciding which of six paddings applied, where two of the branches were unreachable.
The fix wasn't more props — it was a different composition pattern.
Compound components
We split Button into a Button plus a Slot, with the slot rendering whatever the consumer passes. Variants became styling concerns instead of prop concerns.
The rules we settled on, in the order we apply them:
If it changes how the thing looks, it is a variant — one prop, an enum, defined once.
If it changes what the thing contains, it is a child, not a prop.
If it changes what the thing does on click, it is a handler the consumer owns.
If none of the three fit, the component is probably two components.
A worked example
The clearest case was the destructive button. It had three booleans — danger, confirm, and busy — and every combination of the three appeared somewhere in the app, including two that made no sense together.
In the new shape, danger is a variant, confirm is a wrapper component that owns a dialog, and busy is derived from the mutation state the caller already has. The call site went from five props to two and reads like the thing it does.
The subtle win is that the wrapper is now the only place a confirmation dialog is defined. Before, four screens had their own, and three of them had slightly different copy.
What it cost
The migration touched 380 call sites and took three weeks of background work. A codemod handled about 300 of them; the rest were the interesting ones, where a boolean prop had been doing something nobody could explain.
Nine of the fourteen booleans were deleted outright. Two became variants. Three turned out to be features that only one screen used, and they moved into that screen.
What we would do differently
We should have written the four rules before the refactor, not after. Half the review discussion in week two was people rediscovering them one call site at a time.
And we should have deleted the unreachable branches in 2024, when we first noticed them. A dead branch is a decision nobody has made yet.
