Here's the failure sequence, and you've probably lived it: you build a clean Card component. Compound pattern, properly decomposed — Card.Header, Card.Body, Card.Footer. Consumers love it. Then a new design requirement arrives. The marketing team needs a card with a badge in the header. Product needs the image on the left. Someone needs a footer with three actions instead of one. And suddenly you're either adding props back into the compound components, or you're watching teams fork the whole thing and maintain their own variants.
Composition patterns don't fail at creation. They fail at extension.
The Prop Accumulation You Were Trying to Escape
The original sin is well-documented. Mango Developer's recent breakdown of the "black box" problem traces it precisely: a ProductCard starts with title and price, then accumulates badgeText, isLarge, hasImage, imageSrc, variant. Each prop adds a conditional branch. Each branch is a regression surface. The component becomes what they call a "brittle spaghetti monster" — not through negligence, but through the entirely reasonable process of accommodating real product requirements one at a time.
Compound components are the standard fix. Decompose the layout into subcomponents, hand structural control to the consumer, keep styles encapsulated. The DEV Community writeup on reusable component patterns shows the canonical version: Card.Header, Card.Title, Card.Body, Card.Footer as separate exports, each focused, each composable. Consumers assemble what they need. No prop explosion.
This works. Until it doesn't.
Where Compound Patterns Break Down
The problem with compound components is that they trade one kind of coupling for another. As Honcho's glossary on compound vs. composite patterns puts it directly: compound components are "harder to customize — extending Dialog.Header is more work because it's coupled to Dialog's context." The subcomponents aren't independent — they depend on the parent's state and context to function. Dialog.Header without Dialog wrapping it is either broken or meaningless.
That coupling is the point when you need shared state (a Dialog that knows its own viewport, a Tabs component that tracks the active panel). But it becomes the trap when a consumer just needs a slightly different header layout. They can't extract Dialog.Header and modify it independently. They have to either wrap it (adding a layer), fork it (creating drift), or convince you to add a prop (back to square one).
The Shopify Polaris experience is instructive here. Yesenia Perez-Cruz's Beyond Consistency newsletter describes exactly this failure mode at scale: even with a wide component library, "everything started to look the same" because the components were controlling layout and content structure too tightly. Order previews, shipping profiles, and CSV importers — genuinely different information with different cognitive demands — all ended up wearing the same shape because the component made the compositional decisions before the designer could. Polaris eventually moved toward a more composable Card API with open slots. But as Perez-Cruz notes, composability only removes the predetermined outline. It doesn't teach teams what to put in its place.
The Actual Tradeoff You're Making
The compound vs. composite distinction matters here. Honcho's breakdown gives a useful rule of thumb: if subcomponents need shared state to function correctly, use compound. If they're just grouping, use composite. Most modern systems use both — low-level components like Button and Input stay composite (standalone, independently testable), while high-level interactive components like Dialog, Tabs, and Accordion go compound (interdependent by necessity).
The mistake is applying compound patterns where composite would do. When you make Card.Header context-dependent, you've added coupling that the card layout doesn't actually need. Cards don't have shared interactive state. The subcomponents are just grouping. That's a composite job, and treating it as compound is what makes extension painful.
Shadcn/ui's architecture reflects this. The DeepWiki documentation on Base vs. Radix component variants shows how the ecosystem handles this split: Radix-based components use the asChild prop via Slot for element composition, while Base UI variants use render props. Both approaches are trying to solve the same problem — giving consumers control over the rendered element without forcing them to fork the component — but they make different tradeoffs around how tightly the composition is bound.
The Signal to Watch For
The tell that your composition pattern is about to break is when consumers start asking for props on subcomponents. Card.Header getting a withBadge prop is the same failure mode as the original monolithic Card getting hasBadge. You've just moved the prop explosion one level down.
When that happens, the right move usually isn't to add the prop or to tell the consumer to fork. It's to ask whether the subcomponent is doing too much — whether it should be a thinner primitive that exposes its internal structure rather than a styled black box that hides it. The 200OK Solutions analysis of React maintenance debt frames this as the compound interest problem: individually reasonable decisions that were never checked against a plan. No single prop breaks the system. The accumulation does.
The practical check: if a consumer can't achieve a legitimate design variation without modifying your component or adding a prop to it, your primitive boundary is in the wrong place. Move it down until they can.
