Display

Disclosure Group

Accordion-style group of expandable sections built on react-aria-components, styled with the quebi design system. Each item has a cyan-tinted border and white header; expanding lifts the active section with a teal-tinted surface and glow.

accordiondisclosurecollapseexpandinteractive

Default

A single expandable section open at a time.

A copy-paste React component library styled with the quebi design system, built on react-aria-components for an accessible baseline.

Multiple expanded

Allow several sections to stay open at once.

Orders ship within two business days.

Return any item within 30 days for a full refund.

Disabled item

Individual sections can be disabled.

Source

Copy this into your project. Resolve its dependencies from the registryDependencies in the component's API entry.

"use client"

import { use } from "react"
import {
  Button,
  composeRenderProps,
  DisclosureStateContext,
  Heading,
  Disclosure as PrimitiveDisclosure,
  DisclosureGroup as PrimitiveDisclosureGroup,
  DisclosurePanel as PrimitiveDisclosurePanel,
  type ButtonProps,
  type DisclosureGroupProps,
  type DisclosurePanelProps,
  type DisclosureProps,
} from "react-aria-components"
import { cn } from "@/lib/utils"

/**
 * DisclosureGroup — quebi design system
 *
 * Accordion-style stack of expandable sections built on react-aria-components.
 * Each item is bordered with the signature cyan tint; headers read in white and
 * the active item lifts with a subtle teal-tinted surface and glow. Pass
 * `allowsMultipleExpanded` to keep several sections open at once.
 */
export function DisclosureGroup({ className, ...props }: DisclosureGroupProps) {
  return (
    <PrimitiveDisclosureGroup
      data-slot="disclosure-group"
      className={cn("flex flex-col gap-2", className)}
      {...props}
    />
  )
}

export function Disclosure({ className, ...props }: DisclosureProps) {
  return (
    <PrimitiveDisclosure
      data-slot="disclosure"
      className={composeRenderProps(className, (className, { isExpanded, isFocusVisibleWithin }) =>
        cn(
          "group/disclosure w-full overflow-hidden rounded-quebi-md border border-cyan-500/10 bg-quebi-bg transition-colors duration-200",
          "data-[hovered]:border-cyan-500/20",
          (isExpanded || isFocusVisibleWithin) &&
            "border-cyan-500/20 bg-quebi-brand/5 shadow-quebi-glow",
          className,
        ),
      )}
      {...props}
    />
  )
}

export interface DisclosureTriggerProps extends ButtonProps {
  ref?: React.Ref<HTMLButtonElement>
  triggerIndicator?: boolean
}

export function DisclosureTrigger({
  ref,
  className,
  triggerIndicator = true,
  ...props
}: DisclosureTriggerProps) {
  const state = use(DisclosureStateContext)
  if (!state) throw new Error("DisclosureTrigger must be used within a Disclosure")
  return (
    <Heading className="m-0">
      <Button
        {...props}
        ref={ref}
        slot="trigger"
        className={composeRenderProps(className, (className) =>
          cn(
            "flex w-full cursor-pointer items-center justify-between gap-3 px-4 py-3 text-start font-medium text-sm text-white outline-hidden",
            "transition-colors duration-150",
            "data-[hovered]:text-white",
            "data-[focus-visible]:ring-2 data-[focus-visible]:ring-quebi-brand/50 data-[focus-visible]:ring-inset",
            "disabled:opacity-50 disabled:cursor-not-allowed",
            "[&_[data-slot=icon]]:size-4 [&_[data-slot=icon]]:shrink-0",
            className,
          ),
        )}
      >
        {(values) => (
          <>
            {typeof props.children === "function" ? props.children(values) : props.children}
            {triggerIndicator && <DisclosureIndicator />}
          </>
        )}
      </Button>
    </Heading>
  )
}

export function DisclosureIndicator({ className, ...props }: React.ComponentProps<"span">) {
  return (
    <span
      data-slot="disclosure-indicator"
      aria-hidden="true"
      className={cn(
        "pointer-events-none relative flex size-5 shrink-0 items-center justify-center text-quebi-fg-muted [--width:--spacing(2.5)]",
        "group-data-[hovered]/disclosure:text-white",
        className,
      )}
      {...props}
    >
      <span className="absolute h-[1.5px] w-(--width) origin-center rotate-90 bg-current transition-transform duration-300 group-data-[expanded]/disclosure:rotate-0" />
      <span className="absolute h-[1.5px] w-(--width) origin-center bg-current" />
    </span>
  )
}

export function DisclosurePanel({ className, children, ...props }: DisclosurePanelProps) {
  return (
    <PrimitiveDisclosurePanel
      data-slot="disclosure-panel"
      className={cn(
        "overflow-hidden text-sm text-quebi-fg-muted transition-[height] duration-200",
        className,
      )}
      {...props}
    >
      <div
        data-slot="disclosure-panel-content"
        className="px-4 pb-4 text-pretty leading-relaxed"
      >
        {children}
      </div>
    </PrimitiveDisclosurePanel>
  )
}