Skip to content

The Polymorphic Component Dilemma in React

Building components for a design system usually hits a wall the moment product requirements get weird.

2 min read
Polymorphic Components
asChild
TypeScript
React
Accessibility
Design System

Building components for a design system usually hits a wall the moment product requirements get weird. Ideally, a Button should always be a <button>. But in reality, we’re often asked to make something that looks like a button but acts as a link (<a>) or even just a <span> inside a menu.

To handle this, we use polymorphism—usually through an as prop or the asChild pattern you see in Radix UI.

Technically, there are two main ways to go about it. Using an as prop lets the consumer pick any element they want, while asChild just passes props down to an existing child. The problem is that building truly polymorphic components is exhausting, especially when you get into the weeds with TypeScript. You end up wrestling with generics, ComponentPropsWithoutRef, and the headache of managing refs.

Often, instead of forcing a component to be everything to everyone, it makes more sense to just limit the options. For example, you could restrict it so it can only be a button, span, or label.

One crucial thing that gets overlooked is accessibility. This isn't just a TypeScript technicality; it’s a direct risk to the product. When you change a button to an <a>, the user's expectation for how Enter or Space works changes. If you swap it for a div, you lose the original semantics unless you manually rebuild the roles and keyboard navigation. Without being careful, you might end up with a component that looks fine visually but is totally broken for screen reader users.

Not every component needs to be polymorphic. I have a simple rule: if there hasn't been a real need to use more than two different elements with the same look in a single quarter, don't bother. If it’s only ever one element, just keep it fixed.

Sure, making a rigid wrapper feels less flexible. But to me, that cost is much lower than forcing the whole team to learn complex polymorphic types just to build a button. Polymorphism should be a solution for the product, not just a way to show off your TypeScript skills.