Hero image for "Prop Drilling Isn't the Problem. Your Abstraction Is."

Prop Drilling Isn't the Problem. Your Abstraction Is.


There's a reflex in component library work: when you see props being passed through two or three layers, you reach for abstraction. A compound component, a context provider, a configuration object. The prop drilling feels like a code smell, so you treat it like one.

It usually isn't. And the abstraction you build to fix it often creates more friction than the drilling ever did.

When Abstraction Earns Its Weight

Abstraction makes sense when the thing you're hiding is genuinely complex — not just verbose. The clearest example in design system work is accessibility behavior. As one practitioner documented after spending an afternoon trying to write a focus trap from scratch: the first version worked until he tested it with a portal. Then it missed contenteditable elements. Then it broke on Shift+Tab. Then VoiceOver didn't recognize the modal at all.

That's the kind of problem abstraction is built for. The complexity isn't incidental — it's the whole point. Radix's Dialog handles focus locks in portals. Select implements roving tabindex so arrow keys behave the way screen reader users expect. These behaviors exist because a dedicated team has been iterating on real-world bug reports since 2020. Hiding that behind a clean API is a genuine win.

The same logic applies to variant systems. CVA (class-variance-authority) gives you a typed variant surface without a full token library:

const buttonVariants = cva(
  'inline-flex items-center justify-center rounded-md text-sm font-medium',
  {
    variants: {
      variant: {
        default: 'bg-primary text-primary-foreground hover:bg-primary/90',
        destructive: 'bg-destructive text-destructive-foreground',
        ghost: 'hover:bg-accent hover:text-accent-foreground',
      },
      size: {
        default: 'h-10 px-4 py-2',
        sm: 'h-9 rounded-md px-3',
        lg: 'h-11 rounded-md px-8',
      },
    },
  }
)

TypeScript knows which variant and size values are valid. Consumers don't need to know how the classes are constructed. The abstraction is load-bearing — remove it and you get either a stringly-typed mess or a lot of duplicated conditional logic.

When Prop Drilling Is Just Honesty

The problem is that most component APIs don't have that kind of complexity underneath. They have configuration — a few props that control layout, a color, a label. And when you abstract configuration, you don't simplify anything. You just move the decision somewhere the caller can't see it.

I'd argue this is where most design system debt actually originates. A <Card> component that accepts a variant prop instead of padding and border props isn't simpler — it's just less honest about what it's doing. Now when a product team needs a card with slightly different padding, they either request a new variant (your backlog grows) or they override the styles (your system leaks).

The shadcn/ui model is instructive here. When you run npx shadcn-ui@latest add button, you get components/ui/button.tsx in your repo. You own it. The abstraction is transparent by design — you can read it, edit it, and apply upstream diffs selectively. That's a different philosophy than a black-box component library, and it's worth understanding why it resonates: it treats prop drilling and direct modification as acceptable costs in exchange for genuine control.

The alternative — building abstractions before you understand the problem space — has a documented failure mode. Research on platform engineering found that when a global e-commerce company introduced an early "generic transaction handling" abstraction, integrating localized payment flows later required extensive retrofitting and delayed rollout by six months. The abstraction was designed around assumptions, not requirements. When the requirements arrived, the abstraction was in the way.

The Heuristic

Here's the test I'd apply before reaching for abstraction in a component API:

Is the complexity in the implementation, or just in the call site?

If the complexity is in the implementation — accessibility behavior, interaction semantics, variant logic that would otherwise be duplicated — abstraction earns its weight. Hide it. Make the API clean.

If the complexity is just that the call site has a lot of props, that's not complexity. That's configuration. Prop drilling through two layers of a form component isn't a problem to solve; it's a description of what the component actually needs. Abstracting it into a configuration object or a context provider doesn't reduce the configuration — it just makes it harder to trace.

The signal that you've abstracted too early: consumers start working around your API. They override styles, they reach into internals, they ask for escape hatches. That's not user error. That's the abstraction failing to match the actual problem.

Prop drilling that tells the truth about what a component needs is better than an abstraction that hides it. Build the abstraction when you have something genuinely complex to hide — not when the call site looks messy.