Slot
Create polymorphic components with asChild composition for flexible element rendering.
Slot
Slot, createSlot, and isValidElement are utilities for building polymorphic components that support composition via the asChild pattern. This allows components to render different HTML elements while preserving styling and behavior.
The asChild pattern enables composing styled components with custom elements without introducing extra DOM nodes.
Exports
Prop
Type
The asChild pattern
The asChild pattern allows a component to render a different element while preserving the styling and behavior of the original component:
// Button normally renders a <button>
<Button>Click me</Button> // → <button>Click me</button>
// With asChild, render as an <a> instead
<Button asChild>
<a href="/page">Click me</a>
</Button> // → <a href="/page" class="button-styles">Click me</a>The button's classes and behavior are merged onto the <a> element without an extra <button> wrapper.
How Slot works
When asChild={true}, the component uses Slot instead of rendering a normal element. Slot merges all props (className, event handlers, etc.) onto its child:
import { Slot } from '@loke/design-system/slot';
export const Button = forwardRef<
HTMLButtonElement,
ButtonProps & { asChild?: boolean }
>(({ asChild = false, className, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
ref={ref}
className={cn('px-4 py-2 rounded', className)}
{...props}
/>
);
});When asChild={true}, Slot extracts the child element and applies the merged props to it.
Usage
Basic polymorphic component
import { Slot, createSlot } from '@loke/design-system/slot';
import { forwardRef, type ButtonHTMLAttributes } from 'react';
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
asChild?: boolean;
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ asChild = false, className, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
ref={ref}
className={cn('px-4 py-2 rounded bg-primary text-white', className)}
{...props}
/>
);
}
);Using createSlot for display names
import { createSlot } from '@loke/design-system/slot';
const ButtonSlot = createSlot('Button');
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ asChild = false, ...props }, ref) => {
const Comp = asChild ? ButtonSlot : 'button';
return <Comp ref={ref} {...props} />;
}
);Rendering as different elements
// Render button as an anchor
<Button asChild>
<a href="/home">Go Home</a>
</Button>
// Render button as a link from Next.js
<Button asChild>
<Link href="/home">Go Home</Link>
</Button>
// Render as a custom component
<Button asChild>
<NavLink to="/home">Go Home</NavLink>
</Button>Validation with isValidElement
Use isValidElement to safely check if a value is a valid React element before operating on it:
import { isValidElement } from '@loke/design-system/slot';
function MyComponent({ children }) {
if (!isValidElement(children)) {
return <div>Invalid child</div>;
}
return <>{children}</>;
}Props merging behavior
When using Slot, props are intelligently merged:
- className: Merged with
cn()so Tailwind classes don't conflict - style: Merged with style from child taking precedence
- Event handlers: Composed so both parent and child handlers fire
- data- attributes:* Merged, with child values taking precedence
- ref: Forwarded to the underlying element
Example:
<Slot className="px-4" onClick={handleClick}>
<button className="py-2" onClick={handleChildClick}>
Click
</button>
</Slot>
// Results in:
// <button className="px-4 py-2" onClick={composeHandlers(handleClick, handleChildClick)}>
// Click
// </button>Best practices
When to use asChild:
- Rendering components as different elements (links, custom components)
- Avoiding extra DOM nodes when styling other elements
- Building flexible, composable component APIs
Props to forward:
- Always forward
refwhen usingasChild - Document the
asChildprop in your component's API - Ensure merging behavior is intuitive (e.g., className merging)
Common pattern:
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ asChild = false, variant, size, className, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return (
<Comp
ref={ref}
className={cn(
buttonVariants({ variant, size }),
className
)}
{...props}
/>
);
}
);
Button.displayName = 'Button';Examples
Gallery of asChild usage
import { Button } from '@loke/design-system/button';
import Link from 'next/link';
// As anchor
<Button asChild variant="outline">
<a href="/docs">Documentation</a>
</Button>
// As Next.js Link
<Button asChild>
<Link href="/dashboard">Dashboard</Link>
</Button>
// As custom router component
<Button asChild variant="ghost">
<RouterLink to="/settings">Settings</RouterLink>
</Button>
// As form submit button (when button isn't semantic)
<Button asChild type="submit">
<div role="button">Submit</div>
</Button>Technical details
Slot works by:
- Checking if
asChildis true - Extracting the single child React element
- Merging parent props onto the child's props
- Returning the merged child element
The implementation ensures type safety and proper ref handling across different element types.
Import
import { Slot, createSlot, isValidElement } from '@loke/design-system/slot';Or from the ui package directly:
import { Slot, createSlot, isValidElement } from '@loke/ui/slot';