Understanding React's as Prop and Radix UI's asChild Prop

When working with component libraries in React, you might have come across the as prop or Radix UI's asChild prop. These props provide a powerful way to make components more flexible and composable. We'll dive into their differences, how they work, and when to use them.

React's as Prop

The as prop is a pattern commonly used in component libraries to allow developers to dynamically change the rendered element of a component. Instead of wrapping a component in multiple layers of divs or spans, you can control which HTML tag or component is used under the hood.

Here's an example of how the as prop works in a simple button component:

import React from "react";
type ButtonProps = {
as?: React.ElementType;
children: React.ReactNode;
} & React.ComponentPropsWithoutRef<"button">;
const Button: React.FC<ButtonProps> = ({ as: Component = "button", children, ...props }) => {
return <Component {...props}>{children}</Component>;
};
export default function App() {
return (
<>
<Button>Default Button</Button>
<Button as="a" href="#">Rendered as Anchor</Button>
</>
);
}

The simplified Button component above accepts an as prop that determines the underlying element type. When the as prop is not provided, it defaults to rendering a button element.

// A simpler version of our polymorphic button
type ButtonProps = {
as?: React.ElementType; // This allows changing the rendered element
children: React.ReactNode;
variant?: 'primary' | 'secondary';
} & React.ComponentPropsWithoutRef<"button">; // Inherit standard button props
const Button = ({
as: Component = "button", // Default to button if no element specified
children,
variant = 'primary',
...props // Spread remaining props to the rendered element
}: ButtonProps) => {
// Simple className based on variant
const className = `button ${variant}`;
return (
<Component className={className} {...props}>
{children}
</Component>
);
};

Why Use the as Prop?

  • It allows customization of the underlying element without losing functionality.

  • Helps avoid unnecessary wrappers and keeps semantic HTML intact.

  • Works well with accessibility (e.g., using as="a" with appropriate attributes for navigation).

Radix UI's asChild Prop

Radix UI introduces the asChild prop, which behaves differently from the as prop. Instead of changing the rendered element type, it replaces the component's wrapping element with the provided child component.

import { Dialog, DialogTrigger, DialogContent, DialogTitle } from "@radix-ui/react-dialog";
import { Button } from "./Button"; // Assuming we have a custom Button component
export default function AsChildExample() {
return (
<div className="example">
{/* Standard usage - DialogTrigger renders its own button */}
<Dialog>
<DialogTrigger>Open Dialog</DialogTrigger>
<DialogContent>
<DialogTitle>Standard Dialog</DialogTitle>
<p>This uses the default DialogTrigger button</p>
</DialogContent>
</Dialog>
{/* With asChild - composition pattern */}
<Dialog>
<DialogTrigger asChild>
<Button variant="primary" size="large">Custom Open Dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Custom Dialog</DialogTitle>
<p>This uses our custom Button component as the trigger</p>
</DialogContent>
</Dialog>
</div>
);
}

The simplified DialogTrigger component with asChild support might look like this:

type DialogTriggerProps = {
asChild?: boolean;
children: React.ReactNode;
};
const DialogTrigger: React.FC<DialogTriggerProps> = ({ asChild, children, ...props }) => {
// When asChild is true, clone the child and merge the props
if (asChild && React.isValidElement(children)) {
return React.cloneElement(children, {
...props,
...children.props,
});
}
// Default behavior: render a button with the provided props
return (
<button type="button" {...props}>
{children}
</button>
);
};

Why Use the asChild Prop?

  • It enables true component composition, allowing you to leverage your existing component library.

  • Maintains proper accessibility attributes and event handling across component boundaries.

  • Allows you to customize the look and feel of primitive components without sacrificing their built-in functionality.

  • Provides better type safety when used with TypeScript compared to manual prop forwarding.

Conclusion

Both as and asChild are valuable tools for building flexible, composable React components. The as prop lets you change the rendered element while preserving the component structure, whereas asChild fully replaces the wrapper element.