Composition

Build flexible UI with component composition patterns

Overview

HeroUI uses composition patterns to create flexible, customizable components. This allows you to:

  • Change the rendered element or component
  • Compose components together
  • Maintain full control over markup

The asChild Prop

The asChild prop lets you change what element a component renders. When asChild is true, HeroUI won't render its default element - instead, it clones the child element and merges props.

Basic Usage

import { Button } from '@heroui/react';
import Link from 'next/link';

// Renders as a Next.js Link
<Button asChild>
  <Link href="/about">About</Link>
</Button>

// Renders as a regular anchor
<Button asChild>
  <a href="https://example.com">External Link</a>
</Button>

Available Components

These components support asChild:

  • Button - Change button element
  • Alert and its parts - Alert, AlertIcon, AlertContent, AlertTitle, AlertDescription, AlertAction, AlertClose
  • Avatar and its parts - Avatar, AvatarImage, AvatarFallback
  • More components coming soon

Compound Components

HeroUI components are built as compound components - they export multiple parts that work together. You can use them in two ways:

Import each part separately:

import {
  Alert,
  AlertIcon,
  AlertContent,
  AlertTitle,
  AlertDescription,
  AlertClose
} from '@heroui/react';

<Alert>
  <AlertIcon />
  <AlertContent>
    <AlertTitle>Success</AlertTitle>
    <AlertDescription>Your changes have been saved.</AlertDescription>
  </AlertContent>
  <AlertClose />
</Alert>

Option 2: Dot Notation (Compound Pattern)

Import the main component and access parts via dot notation:

import { Alert } from '@heroui/react';

<Alert.Root>
  <Alert.Icon />
  <Alert.Content>
    <Alert.Title>Success</Alert.Title>
    <Alert.Description>Your changes have been saved.</Alert.Description>
  </Alert.Content>
  <Alert.Close />
</Alert.Root>

Note: When using dot notation, the main component requires .Root suffix (e.g., <Alert.Root> instead of <Alert>).

Benefits of Compound Components

Both patterns provide:

  • Flexibility - Arrange parts as needed
  • Customization - Style each part independently
  • Control - Add or remove parts

Style Variants

HeroUI exports variant functions that can be applied to any component. This allows you to use HeroUI's design system with any element or component:

Using buttonVariants

Apply button styles to any component:

import { Link, LinkIcon, buttonVariants } from '@heroui/react';

// Link styled as a tertiary button
<Link.Root
  className={buttonVariants({
    size: "md",
    variant: "tertiary",
    className: "px-3"
  })}
  href="https://heroui.com"
  target="_blank"
>
  HeroUI
  <LinkIcon className="h-2 w-2" />
</Link.Root>

// Native anchor styled as primary button
<a
  className={buttonVariants({ variant: "primary" })}
  href="/dashboard"
>
  Go to Dashboard
</a>

Available Variant Functions

Each component exports its variant function:

  • buttonVariants - Button styles
  • chipVariants - Chip styles
  • linkVariants - Link styles
  • spinnerVariants - Spinner styles
  • More variant functions for each component

Custom Components

Create your own components by composing HeroUI primitives:

import { Button, Tooltip } from '@heroui/react';

// Link button component
function LinkButton({ href, children, ...props }) {
  return (
    <Button asChild {...props}>
      <a href={href}>{children}</a>
    </Button>
  );
}

// Icon button with tooltip
function IconButton({ icon, label, ...props }) {
  return (
    <Tooltip.Root>
      <Tooltip.Trigger>
        <Button isIconOnly {...props}>
          <Icon icon={icon} />
        </Button>
      </Tooltip.Trigger>
      <Tooltip.Content>{label}</Tooltip.Content>
    </Tooltip.Root>
  );
}

Custom Variants

You can create your own custom variants by extending the component's variant function.

import type {ButtonProps} from "@heroui/react";
import type {VariantProps} from "tailwind-variants";

import {Button, buttonVariants} from "@heroui/react";
import {tv} from "tailwind-variants";

const myButtonVariants = tv({
  extend: buttonVariants,
  base: "text-md text-shadow-lg font-semibold shadow-md data-[pending=true]:opacity-40",
  variants: {
    radius: {
      lg: "rounded-lg",
      md: "rounded-md",
      sm: "rounded-sm",
      full: "rounded-full",
    },
    size: {
      sm: "h-10 px-4",
      md: "h-11 px-6",
      lg: "h-12 px-8",
      xl: "h-13 px-10",
    },
    variant: {
      primary: "text-white dark:bg-white/10 dark:text-white dark:hover:bg-white/15",
    },
  },
  defaultVariants: {
    radius: "full",
    variant: "primary",
  },
});

type MyButtonVariants = VariantProps<typeof myButtonVariants>;
export type MyButtonProps = Omit<ButtonProps, "className"> &
  MyButtonVariants & {className?: string};

function CustomButton({className, radius, variant, ...props}: MyButtonProps) {
  return <Button className={myButtonVariants({className, radius, variant})} {...props} />;
}

export function CustomVariants() {
  return <CustomButton>Custom Button</CustomButton>;
}

With Next.js

Use asChild to integrate with Next.js:

import Link from 'next/link';
import { Button } from '@heroui/react';

<Button asChild variant="primary">
  <Link href="/dashboard">Dashboard</Link>
</Button>

With React Router

Use asChild to integrate with routing libraries:

import { Link } from 'react-router-dom';
import { Button } from '@heroui/react';

<Button asChild variant="primary">
  <Link to="/dashboard">Dashboard</Link>
</Button>

Next Steps