DEV Community

Talisson
Talisson

Posted on

Slot-Based APIs in React: Designing Flexible and Composable Components

In UI development, flexibility is everything. You want to give developers control over what content they render while maintaining how it's structured and styled.

Traditional component APIs often fall into the trap of prop bloat — passing specific props like headerContent, footerActions, or leftIcon. As components grow, these APIs become rigid and cumbersome.

A better pattern? Slot-Based APIs.


What Are Slots in UI Components?

A Slot is a named placeholder within a component's structure where developers can inject custom content.

It’s like giving developers controlled "hooks" inside your component's layout:

“Here’s a spot where you can put custom content — and it will render in the right place, with consistent styling.”

Think of slots as the content injection points of a component.


Why Slot-Based APIs Are Powerful

Problem Slot-Based API Solution
Components with too many specific props Replace with flexible named slots
Rigid structure, hard to customize content Allow arbitrary JSX to be injected in slots
Props don’t scale with complex components Slots decouple structure from content
Need for layout consistency with content flexibility Slots enforce structure but let content be dynamic

Real-World Example: Card Component

Props-Driven API (Rigid & Bloated)

<Card
  title="User Profile"
  description="User details"
  headerIcon={<UserIcon />}
  footerActions={<Button>Edit</Button>}
/>
Enter fullscreen mode Exit fullscreen mode

Slot-Based API (Flexible & Scalable)

<Card>
  <Card.Header>
    <UserIcon />
    <h2>User Profile</h2>
  </Card.Header>
  <Card.Body>
    User details go here.
  </Card.Body>
  <Card.Footer>
    <Button>Edit</Button>
  </Card.Footer>
</Card>
Enter fullscreen mode Exit fullscreen mode

How to Implement Slot-Based APIs in React

1. Compound Components + Context (Recommended)

const CardContext = createContext({});

function Card({ children }: { children: React.ReactNode }) {
  return <CardContext.Provider value={{}}>{children}</CardContext.Provider>;
}

function CardHeader({ children }: { children: React.ReactNode }) {
  return <div className="card-header">{children}</div>;
}

function CardBody({ children }: { children: React.ReactNode }) {
  return <div className="card-body">{children}</div>;
}

function CardFooter({ children }: { children: React.ReactNode }) {
  return <div className="card-footer">{children}</div>;
}

Card.Header = CardHeader;
Card.Body = CardBody;
Card.Footer = CardFooter;
Enter fullscreen mode Exit fullscreen mode

2. React.Children & cloneElement (Positional Slots)

function Modal({ children }: { children: React.ReactNode }) {
  const header = React.Children.toArray(children).find(child => React.isValidElement(child) && child.type === ModalHeader);
  const footer = React.Children.toArray(children).find(child => React.isValidElement(child) && child.type === ModalFooter);
  const body = React.Children.toArray(children).filter(child => child !== header && child !== footer);

  return (
    <div className="modal">
      {header}
      <div className="modal-body">{body}</div>
      {footer}
    </div>
  );
}

function ModalHeader({ children }: { children: React.ReactNode }) {
  return <div className="modal-header">{children}</div>;
}

function ModalFooter({ children }: { children: React.ReactNode }) {
  return <div className="modal-footer">{children}</div>;
}
Enter fullscreen mode Exit fullscreen mode

3. Advanced Slot Components (Radix UI's Slot and React Aria's Slot Patterns)

Radix UI's Slot Component

Radix provides a Slot component that forwards props, refs, and classNames to its child element.

import { Slot } from '@radix-ui/react-slot';

function IconButton({ children }: { children: React.ReactNode }) {
  return (
    <button className="icon-button">
      <Slot className="button-icon" />
      {children}
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

Example: asChild API

function Button({ asChild = false, ...props }: { asChild?: boolean } & React.ComponentProps<'button'>) {
  const Comp = asChild ? Slot : 'button';
  return <Comp className="button" {...props} />;
}

export default function Example() {
  return (
    <Button asChild>
      <a href="/contact">Contact</a>
    </Button>
  );
}
Enter fullscreen mode Exit fullscreen mode

Handling Event Handlers in Radix Slot

Radix Slot merges event handlers, prioritizing the child’s handler.

<Button asChild onClick={(e) => {
  console.log('from Slot');
  if (e.defaultPrevented) return;
}}>
  <a href="/contact" onClick={(e) => {
    e.preventDefault();
    console.log('from Child');
  }}>Contact</a>
</Button>
Enter fullscreen mode Exit fullscreen mode

React Aria's Slot System (Slot Prop Injection)

React Aria Components use a slot prop pattern to inject ARIA roles and behaviors.

import { Dialog, Heading, Button } from 'react-aria-components';

function ExampleDialog() {
  return (
    <Dialog>
      <Heading slot="title">Settings</Heading>
      <p>Dialog content here</p>
      <Button slot="close">Close</Button>
    </Dialog>
  );
}
Enter fullscreen mode Exit fullscreen mode

Props and accessibility attributes are merged internally based on the slot prop.


Slot Component: Radix UI vs React Aria

Library Slot Mechanism Purpose
Radix UI <Slot> component that forwards props, refs, and classNames to its child. Flexible composition, element-level prop merging.
React Aria Context-based slot props injection system (no visual component). Accessibility-focused, named prop injection into structured components.

Best Practices

  • Name slots meaningfully (Header, Footer, etc.).
  • Avoid prop bloat by favoring compositional slots.
  • Ensure slot content inherits design tokens.
  • Document slot usage in your design system.

References


Do you use Slot-Based APIs in your projects? Share your patterns and thoughts below!

Top comments (0)