Bloom Logo

Accordion

A vertically stacked set of interactive headings that expand or collapse associated content sections, supporting controlled states, custom icons, and disabled items.

importimport { Accordion } from "@/components/ui/accordion/accordion";
$npx @bloomui-react/cli add accordion
accordion.tsx
"use client";

import { Icon } from "@iconify/react";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import * as React from "react";
import { cn } from "@/lib/utils";

type AccordionVariant =
  | "default"
  | "bordered"
  | "splitted"
  | "shadow"
  | "compact";

interface AccordionContextValue {
  variant: AccordionVariant;
  isKeepMounted?: boolean;
  showDividers?: boolean;
}

const AccordionContext = React.createContext<AccordionContextValue>({
  variant: "default",
  isKeepMounted: false,
  showDividers: true,
});

const useAccordionContext = () => React.useContext(AccordionContext);

type AccordionProps = React.ComponentPropsWithoutRef<
  typeof AccordionPrimitive.Root
> & {
  variant?: AccordionVariant;
  isDisabled?: boolean;
  isKeepMounted?: boolean;
  showDividers?: boolean;
};

const Accordion = React.forwardRef<
  React.ElementRef<typeof AccordionPrimitive.Root>,
  AccordionProps
>(
  (
    {
      className,
      variant = "default",
      isDisabled,
      disabled,
      isKeepMounted = false,
      showDividers = true,
      ...props
    },
    ref,
  ) => {
    const isAccordionDisabled = isDisabled || disabled;

    const rootClasses = cn(
      variant === "bordered" &&
        "border border-zinc-200 dark:border-zinc-800 rounded-xl p-1 bg-white dark:bg-zinc-900",
      variant === "shadow" &&
        "shadow-md rounded-xl bg-white dark:bg-zinc-900 border border-zinc-200/60 dark:border-zinc-800/60 p-1",
      variant === "splitted" && "space-y-2",
      isAccordionDisabled && "opacity-60 pointer-events-none select-none",
      className,
    );

    return (
      <AccordionContext.Provider
        value={{ variant, isKeepMounted, showDividers }}
      >
        <AccordionPrimitive.Root
          ref={ref}
          disabled={isAccordionDisabled}
          className={rootClasses}
          {...props}
        />
      </AccordionContext.Provider>
    );
  },
);
Accordion.displayName = "Accordion";

type AccordionItemProps = React.ComponentPropsWithoutRef<
  typeof AccordionPrimitive.Item
> & {
  isDisabled?: boolean;
};

const AccordionItem = React.forwardRef<
  React.ElementRef<typeof AccordionPrimitive.Item>,
  AccordionItemProps
>(({ className, isDisabled, disabled, ...props }, ref) => {
  const { variant, showDividers } = useAccordionContext();
  const isItemDisabled = isDisabled || disabled;

  return (
    <AccordionPrimitive.Item
      ref={ref}
      disabled={isItemDisabled}
      className={cn(
        variant === "default" &&
          cn(
            "px-4",
            showDividers &&
              "border-b border-zinc-200 dark:border-zinc-800 last:border-b-0",
          ),
        variant === "bordered" &&
          cn(
            "px-4",
            showDividers &&
              "border-b border-zinc-200 dark:border-zinc-800 last:border-b-0",
          ),
        variant === "splitted" &&
          "bg-white dark:bg-zinc-900 rounded-xl border border-zinc-200/80 dark:border-zinc-800/80 shadow-xs px-4",
        variant === "shadow" &&
          cn(
            "px-4",
            showDividers &&
              "border-b border-zinc-200/40 dark:border-zinc-800/40 last:border-b-0",
          ),
        variant === "compact" &&
          cn(
            "px-3",
            showDividers &&
              "border-b border-zinc-200 dark:border-zinc-800 last:border-b-0",
          ),
        isItemDisabled &&
          "opacity-50 pointer-events-none data-[disabled]:opacity-50",
        className,
      )}
      {...props}
    />
  );
});
AccordionItem.displayName = "AccordionItem";

type AccordionTriggerProps = React.ComponentPropsWithoutRef<
  typeof AccordionPrimitive.Trigger
> & {
  startContent?: React.ReactNode;
  endContent?: React.ReactNode;
  hideIndicator?: boolean;
  isDisabled?: boolean;
};

const AccordionTrigger = React.forwardRef<
  React.ElementRef<typeof AccordionPrimitive.Trigger>,
  AccordionTriggerProps
>(
  (
    {
      className,
      children,
      startContent,
      endContent,
      hideIndicator = false,
      isDisabled,
      disabled,
      ...props
    },
    ref,
  ) => {
    const { variant } = useAccordionContext();
    const isTriggerDisabled = isDisabled || disabled;

    return (
      <AccordionPrimitive.Header className="flex">
        <AccordionPrimitive.Trigger
          ref={ref}
          disabled={isTriggerDisabled}
          className={cn(
            "flex flex-1 items-center justify-between gap-3 text-sm font-medium text-zinc-900 dark:text-zinc-100 transition-all hover:underline text-left outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm cursor-pointer",
            "disabled:pointer-events-none disabled:opacity-50",
            "[&[data-state=open]_.accordion-indicator]:rotate-180",
            variant === "compact" ? "py-2.5" : "py-4",
            className,
          )}
          {...props}
        >
          <div className="flex items-center gap-3 flex-1 min-w-0">
            {startContent && (
              <span className="shrink-0 inline-flex items-center text-zinc-500 dark:text-zinc-400">
                {startContent}
              </span>
            )}
            <span className="truncate">{children}</span>
          </div>
          {!hideIndicator && (
            <span className="accordion-indicator shrink-0 transition-transform duration-200 text-zinc-500 dark:text-zinc-400 flex items-center justify-center">
              {endContent ?? (
                <Icon icon="hugeicons:arrow-down-01" className="size-4" />
              )}
            </span>
          )}
        </AccordionPrimitive.Trigger>
      </AccordionPrimitive.Header>
    );
  },
);
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;

type AccordionContentProps = React.ComponentPropsWithoutRef<
  typeof AccordionPrimitive.Content
> & {
  forceMount?: boolean;
};

const AccordionContent = React.forwardRef<
  React.ElementRef<typeof AccordionPrimitive.Content>,
  AccordionContentProps
>(({ className, children, forceMount, ...props }, ref) => {
  const { variant, isKeepMounted } = useAccordionContext();
  const shouldForceMount = forceMount || isKeepMounted;

  return (
    <AccordionPrimitive.Content
      ref={ref}
      forceMount={shouldForceMount ? true : undefined}
      className="overflow-hidden text-sm transition-all data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down"
      {...props}
    >
      <div
        className={cn(
          variant === "compact" ? "pb-3 pt-0" : "pb-4 pt-1",
          "text-zinc-600 dark:text-zinc-400 leading-relaxed",
          className,
        )}
      >
        {children}
      </div>
    </AccordionPrimitive.Content>
  );
});
AccordionContent.displayName = AccordionPrimitive.Content.displayName;

export type {
  AccordionContentProps,
  AccordionItemProps,
  AccordionProps,
  AccordionTriggerProps,
  AccordionVariant,
};
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };

Default

A standard, uncontrolled accordion allowing vertical expansion and collapse of content sections.

type: single | multiplecollapsible: boolean

Yes. It adheres to WAI-ARIA standards and handles keyboard navigation automatically.

Variants

Choose from multiple visual styles using the variant prop (default, bordered, splitted, shadow, compact).

variant: default | bordered | splitted | shadow | compact
variant="default"

Standard style with clean, subtle bottom dividers.
variant="bordered"

Enclosed by a clean outer border frame.
variant="splitted"

Each panel rendered as an independent card with spacing.

variant="shadow"

Features moderate drop shadows for prominent interface hierarchy.
variant="compact"

Reduced vertical padding designed for high-density layouts.

Controlled

Control which panel is currently open programmatically using the value and onValueChange props.

value: string | string[]onValueChange: (value: any) => void
Active value: item-2

Configure two-factor authentication, change your password, and view active sessions.

Start & End Icons

Add custom icons before the title using startContent, or customize/replace the right-side arrow indicator using endContent.

startContent: ReactNodeendContent: ReactNodehideIndicator: boolean

Update your display name, contact email, and profile avatar.

Disabled State

Disable specific accordion items or an entire accordion block using the isDisabled (or disabled) prop.

isDisabled: boolean

Specific Disabled Item

Access to up to 3 projects and community support.

Entire Accordion Disabled

Without Dividers

Remove the separator lines between accordion items by setting the showDividers prop to false.

showDividers: boolean

Notice there is no line separating this item from the next one.

Props — Accordion

Properties for configuring the Root Accordion container.

PropTypeDefaultDescription
type'single' | 'multiple''single'Determines whether single or multiple items can be opened simultaneously.
valuestring | string[]Controlled open state value managed via React useState.
defaultValuestring | string[]The initial value of item(s) to expand when uncontrolled.
onValueChange(value: any) => voidEvent handler called when the expanded state changes.
variant'default' | 'bordered' | 'splitted' | 'shadow' | 'compact''default'Visual variant style of the Accordion container.
collapsiblebooleanfalseWhen type is 'single', allows closing an open item by clicking its trigger again.
isDisabledbooleanfalseDisables interaction with all accordion items.
showDividersbooleantrueDetermines if borders/dividers are shown between items.

Props — AccordionItem

Properties for configuring individual Accordion item wrappers.

PropTypeDefaultDescription
valuestringA unique value identifying the item panel.
isDisabledbooleanfalseDisables interaction with this specific item.

Props — AccordionTrigger

Properties for configuring the Accordion header trigger element.

PropTypeDefaultDescription
startContentReactNodeElement (such as an icon) rendered before the trigger title.
endContentReactNodeCustom element or icon that replaces the default right-hand indicator arrow.
hideIndicatorbooleanfalseHides the right-hand arrow indicator completely.
isDisabledbooleanfalseDisables this individual trigger element.