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.

accordion.tsx

Accordion component powered by Radix Primitives supporting controlled state, start/end content icons, disabled states, and visual variants.

ReactRadix UITailwindUI ComponentAccordion
"use client";

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

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

interface AccordionContextValue {
  variant: AccordionVariant;
}

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

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

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

const Accordion = React.forwardRef<
  React.ElementRef<typeof AccordionPrimitive.Root>,
  AccordionProps
>(({ className, variant = "default", isDisabled, disabled, ...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 }}>
      <AccordionPrimitive.Root
        ref={ref}
        disabled={isAccordionDisabled}
        className={rootClasses}
        {...props}
      />
    </AccordionContext.Provider>
  );
});
Accordion.displayName = "Accordion";

/* ─── Item ─── */
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 } = useAccordionContext();
  const isItemDisabled = isDisabled || disabled;

  return (
    <AccordionPrimitive.Item
      ref={ref}
      disabled={isItemDisabled}
      className={cn(
        variant === "default" && "border-b border-zinc-200 dark:border-zinc-800 last:border-b-0 px-4",
        variant === "bordered" && "border-b border-zinc-200 dark:border-zinc-800 last:border-b-0 px-4",
        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" && "border-b border-zinc-200/40 dark:border-zinc-800/40 last:border-b-0 px-4",
        variant === "compact" && "border-b border-zinc-200 dark:border-zinc-800 last:border-b-0 px-3",
        isItemDisabled && "opacity-50 pointer-events-none data-[disabled]:opacity-50",
        className
      )}
      {...props}
    />
  );
});
AccordionItem.displayName = "AccordionItem";

/* ─── Trigger ─── */
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",
            "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;

/* ─── Content ─── */
type AccordionContentProps = React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>;

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

  return (
    <AccordionPrimitive.Content
      ref={ref}
      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 { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
export type {
  AccordionVariant,
  AccordionProps,
  AccordionItemProps,
  AccordionTriggerProps,
  AccordionContentProps,
};

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.

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 (startContent & endContent)

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 (isDisabled)

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

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.
API Reference

Props — Accordion (Root)

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.

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.