- React
- Advanced Component Design Patterns and Error Boundaries
- Polymorphic Components & The "as" Prop Pattern
Polymorphic Components & The "as" Prop Pattern
A Polymorphic Component is a dynamic UI primitive that can render as different HTML tags or React components using an "as" prop, while preserving native semantic HTML attributes, accessibility, and visual styling without code duplication.
Key Learning Objectives
Design flexible polymorphic UI primitives (Button, Text, Box) using the "as" prop pattern.
Preserve semantic HTML for accessibility (e.g. rendering a button visually while outputting an <a> anchor tag).
Forward refs cleanly to dynamic underlying DOM elements.
Explain how TypeScript handles type inference for dynamic HTML attributes based on the "as" prop.
The Interview Problem & Context
Implement a polymorphic <Button> component that defaults to `<button>` but can render as `<a>`, forwarding refs and native HTML attributes properly.
1import React, { ElementType, forwardRef } from 'react';23// Polymorphic props type definition4type PolymorphicProps<E extends ElementType, P = {}> = P & {5 as?: E;6} & Omit<React.ComponentPropsWithRef<E>, keyof P | 'as'>;78interface ButtonBaseProps {9 variant?: 'primary' | 'secondary' | 'danger';10 children: React.ReactNode;11}1213// Polymorphic component definition14export const Button = forwardRef(function Button<E extends ElementType = 'button'>(15 { as, variant = 'primary', children, ...restProps }: PolymorphicProps<E, ButtonBaseProps>,16 ref: any17) {18 // 1. Dynamic component tag assignment19 const Component = as || 'button';2021 return (22 <Component23 ref={ref}24 className={`btn btn-${variant}`}25 {...restProps}26 >27 {children}28 </Component>29 );30});3132// Example Usage:33export function Navigation() {34 return (35 <div className="nav-actions">36 {/* 1. Renders as native <button> */}37 <Button variant="primary" onClick={() => alert('Clicked')}>38 Submit Action39 </Button>4041 {/* 2. Renders as native <a> with href, target */}42 <Button as="a" href="https://example.com" target="_blank" variant="secondary">43 External Link44 </Button>45 </div>46 );47}
Deep Architectural Explanation
Why Polymorphic Components are Essential
In modern design systems, visual appearance often diverges from semantic HTML:
- A link needs to look like a prominent button.
- A button needs to look like plain text.
- If developers misuse
<button onClick={() => window.location.href = ...}>, users lose middle-click opening, right-click "Open in new tab", bookmarking, and screen readers announce a button instead of a link!
How the Pattern Works:
- Dynamic Element Resolution: JSX requires custom element types to be capitalized (
<Component />). We assignconst Component = as || 'button'. - Prop Forwarding: All remaining props (
href,target,type,onClick) are spread onto the underlying element. - Accessibility Win: Visual styles remain 100% consistent via CSS classes, while the rendered DOM tree uses the semantically correct HTML tag.
Common Interview Traps & Pitfalls
Lowercasing the dynamic tag variable in JSX (`<as />`), which React compiles as a literal `<as>` HTML tag rather than evaluating the variable.
Creating fake buttons that navigate via JavaScript instead of using `as="a"`.
Losing type safety by using `any` for props instead of generic polymorphic types.
Senior Interviewer Follow-Up Probes
How do modern libraries like Radix UI implement polymorphic behavior using the `asChild` Slot pattern instead?
How do you write the complete TypeScript type with ref forwarding without using `any`?
What are the performance impacts of dynamic element creation in large lists?
Frequently Asked Interview Questions
Why is the "as" prop important for accessibility (a11y)?
It ensures interactive elements use semantic HTML tags. Navigation controls can render as <a> anchors (supporting middle-click and screen readers) while retaining button visual design.
Why does JSX require the variable name to be capitalized when rendering dynamic tags?
JSX treats lowercase tags (like <div> or <as>) as built-in HTML strings. Uppercase tags (<Component>) tell JSX to look up a JavaScript variable or component in scope.
What is the modern alternative to the "as" prop in Radix UI?
The "asChild" slot pattern, where rather than passing a tag name string, the consumer passes the desired element as a child, and the parent clones it while merging props and event handlers.
Can the "as" prop accept custom React components instead of HTML strings?
Yes. You can pass custom components like Next.js Link or React Router Link (`as={Link}`), and the polymorphic component will forward props to that component.
How does ref forwarding work with polymorphic components?
The ref type must be parameterized generically with the element type E, so that the forwarded ref matches the specific DOM element (e.g. HTMLAnchorElement vs HTMLButtonElement).
