Skip to content
Components

Accordion

Organizes related details into expandable sections without leaving the page.

Orders placed before 2 PM usually leave our studio the same business day. We will email tracking details as soon as the carrier scans it.

Playground

allowsMultipleExpanded

Allows more than one item to remain expanded.

isDisabled

Disables every disclosure in the group.

Usage

tsx
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "earthling-ui/accordion";

export function Example() {
  return (
    <>
      <Accordion defaultExpandedKeys={["shipping"]}>
        <AccordionItem id="shipping">
          <AccordionTrigger>When will my order ship?</AccordionTrigger>
          <AccordionContent>
            Orders usually ship within one business day.
          </AccordionContent>
        </AccordionItem>
      </Accordion>
    </>
  );
}
  • Use concise trigger labels that describe the content revealed below them.
  • Single expansion is the default; enable multiple sections only when comparing answers is useful.
  • React Aria provides keyboard navigation and expanded-state semantics through the trigger buttons.

Installation

Start with the project setup for React and Tailwind CSS 4. Both paths use the same component and theme.

bash
npm install earthling-ui

Use the component import shown above. Package updates keep the implementation current.

API reference

Declarations from the current library build, including inherited primitive props. Playground controls above show selected options; they are not the complete API.

tsx
// Generated by dts-bundle-generator v9.5.1

export declare const Accordion: import("react").ForwardRefExoticComponent<Omit<import("react-aria-components").DisclosureGroupProps & import("react").RefAttributes<HTMLDivElement>, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
export declare const AccordionItem: import("react").ForwardRefExoticComponent<Omit<import("react-aria-components").DisclosureProps & import("react").RefAttributes<HTMLDivElement>, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
export declare const AccordionTrigger: import("react").ForwardRefExoticComponent<Omit<import("react-aria-components").ButtonProps & import("react").RefAttributes<HTMLButtonElement>, "ref"> & import("react").RefAttributes<HTMLButtonElement>>;
export declare const AccordionContent: import("react").ForwardRefExoticComponent<Omit<import("react-aria-components").DisclosurePanelProps & import("react").RefAttributes<HTMLDivElement>, "ref"> & import("react").RefAttributes<HTMLDivElement>>;

export {};

Runtime dependencies: cnfast, react-aria-components.

Source

Use the eject command to copy this implementation with its required helpers and project-specific imports.

View Accordion implementation
tsx
"use client";

import {
  forwardRef,
  type ComponentPropsWithoutRef,
  type ComponentRef,
} from "react";
import { cn } from "@/utils/cn";
import {
  DisclosureGroup,
  Disclosure,
  DisclosurePanel,
  Header,
  Button,
  composeRenderProps,
} from "react-aria-components";

// Accordion
const Accordion = forwardRef<
  ComponentRef<typeof DisclosureGroup>,
  ComponentPropsWithoutRef<typeof DisclosureGroup>
>(({ className, ...props }, ref) => (
  <DisclosureGroup
    ref={ref}
    {...props}
    className={composeRenderProps(className, (className) =>
      cn("w-full", className),
    )}
  />
));
Accordion.displayName = "Accordion";

// AccordionItem
const AccordionItem = forwardRef<
  ComponentRef<typeof Disclosure>,
  ComponentPropsWithoutRef<typeof Disclosure>
>(({ className, ...props }, ref) => (
  <Disclosure
    ref={ref}
    className={composeRenderProps(className, (className) =>
      cn("border-b last:border-b-0", className),
    )}
    {...props}
  />
));
AccordionItem.displayName = "AccordionItem";

// AccordionTrigger
const AccordionTrigger = forwardRef<
  ComponentRef<typeof Button>,
  ComponentPropsWithoutRef<typeof Button>
>(({ className, children, ...props }, ref) => (
  <Header className="flex">
    <Button
      slot="trigger"
      ref={ref}
      className={composeRenderProps(className, (className) =>
        cn(
          "group flex flex-1 cursor-pointer items-center justify-between gap-3 rounded-sm py-4 text-left font-medium outline-none hover:underline focus-visible:ring-2 focus-visible:ring-outline",
          className,
        ),
      )}
      {...props}
    >
      {typeof children === "function" ? (
        children
      ) : (
        <>
          {children}
          <i
            aria-hidden="true"
            className="icon-[lucide--chevron-down] shrink-0 transition-transform duration-200 ease-out group-aria-[expanded=true]:rotate-180 motion-reduce:transition-none"
          />
        </>
      )}
    </Button>
  </Header>
));
AccordionTrigger.displayName = "AccordionTrigger";

// AccordionContent
const AccordionContent = forwardRef<
  ComponentRef<typeof DisclosurePanel>,
  ComponentPropsWithoutRef<typeof DisclosurePanel>
>(({ className, children, ...props }, ref) => (
  <DisclosurePanel
    ref={ref}
    className={composeRenderProps(className, (className) =>
      cn(
        "h-[var(--disclosure-panel-height)] overflow-hidden text-sm transition-[height] duration-200 ease-out motion-reduce:transition-none",
        className,
      ),
    )}
    {...props}
  >
    <div className="pb-4 pt-0">{children}</div>
  </DisclosurePanel>
));

AccordionContent.displayName = "AccordionContent";

export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };