For the complete documentation index, see llms.txt. Markdown variants are available by appending .md to documentation URLs. Homepage markdown is available at /llms.md/content.md. An agent skill is available at /.well-known/agent-skills/site-skill.md.

Accordion

The Accordion component from this design system, built on Base UI.

Installation

$ pnpm dlx shadcn@latest add @cobalt/accordion

Source

"use client";

import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion";
import { RiArrowDownSLine, RiAddLine } from "@remixicon/react";
import { cn } from "cn";
import * as React from "react";

type AccordionVariant = "divided" | "contained" | "separated";
const Context = React.createContext<{
  variant: AccordionVariant;
  indicator: "chevron" | "plus";
}>({ variant: "divided", indicator: "chevron" });

function Accordion({
  className,
  variant = "divided",
  indicator = "chevron",
  ...props
}: AccordionPrimitive.Root.Props & {
  variant?: AccordionVariant;
  indicator?: "chevron" | "plus";
}) {
  return (
    <Context.Provider value={{ variant, indicator }}>
      <AccordionPrimitive.Root
        data-slot="accordion"
        className={cn(
          "flex w-full flex-col",
          variant === "contained" &&
            "gap-[3px] rounded-[26px] bg-muted p-[3px]",
          variant === "separated" && "gap-2",
          className
        )}
        {...props}
      />
    </Context.Provider>
  );
}
function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) {
  const { variant } = React.useContext(Context);
  return (
    <AccordionPrimitive.Item
      data-slot="accordion-item"
      className={cn(
        "group/accordion-item data-disabled:opacity-50",
        variant === "divided" &&
          "not-last:border-b not-last:border-[var(--surface-strong,var(--border))]",
        variant === "contained" &&
          "rounded-[23px] bg-[var(--accordion-card,var(--background))]",
        variant === "separated" &&
          "rounded-[16px] border border-border bg-[var(--surface-overlay,var(--background))] shadow-[shadow:var(--shadow-control,0_1px_2px_0_rgb(0_0_0/0.05))]",
        className
      )}
      {...props}
    />
  );
}
function AccordionTrigger({
  className,
  children,
  description,
  icon: LeadingIcon,
  ...props
}: AccordionPrimitive.Trigger.Props & {
  /** Supporting line under the title. */
  description?: React.ReactNode;
  /** Icon before the title. */
  icon?: React.ComponentType<{ className?: string }>;
}) {
  const { variant, indicator } = React.useContext(Context);
  const Icon = indicator === "plus" ? RiAddLine : RiArrowDownSLine;
  return (
    <AccordionPrimitive.Header className="flex">
      <AccordionPrimitive.Trigger
        data-slot="accordion-trigger"
        className={cn(
          "group/accordion-trigger flex w-full cursor-pointer items-center gap-3 text-start text-[16px] leading-[22px] font-medium text-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset data-disabled:cursor-not-allowed",
          variant === "divided" && "rounded-[8px] py-4",
          variant === "contained" && "rounded-[23px] px-5 py-4",
          variant === "separated" && "rounded-[16px] px-4 py-3.5",
          className
        )}
        {...props}
      >
        {LeadingIcon ? (
          <LeadingIcon
            aria-hidden
            className="size-5 shrink-0 text-[color:var(--foreground-secondary,var(--muted-foreground))]"
          />
        ) : null}
        {description ? (
          <span className="flex min-w-0 flex-1 flex-col gap-0.5">
            <span>{children}</span>
            <span className="text-sm leading-5 font-normal text-[color:var(--foreground-tertiary,var(--muted-foreground))]">
              {description}
            </span>
          </span>
        ) : (
          children
        )}
        <Icon
          aria-hidden
          data-slot="accordion-trigger-icon"
          className={cn(
            "ml-auto size-5 shrink-0 text-[color:var(--foreground-subtle,var(--muted-foreground))] transition-[rotate,color] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] group-hover/accordion-trigger:text-[color:var(--foreground-secondary,var(--muted-foreground))] group-data-panel-open/accordion-trigger:text-foreground motion-reduce:transition-none",
            indicator === "plus"
              ? "group-data-panel-open/accordion-trigger:rotate-45"
              : "group-data-panel-open/accordion-trigger:rotate-180"
          )}
        />
      </AccordionPrimitive.Trigger>
    </AccordionPrimitive.Header>
  );
}
function AccordionContent({
  className,
  children,
  ...props
}: AccordionPrimitive.Panel.Props) {
  const { variant } = React.useContext(Context);
  return (
    <AccordionPrimitive.Panel
      data-slot="accordion-content"
      className="h-(--accordion-panel-height) overflow-clip transition-[height] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] data-ending-style:h-0 data-starting-style:h-0 motion-reduce:transition-none"
      {...props}
    >
      <div
        className={cn(
          "text-sm leading-5 font-normal text-[color:var(--orders-chart-label,var(--muted-foreground))] opacity-0 transition-[opacity,translate,filter] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] motion-safe:-translate-y-1 motion-safe:blur-[4px] group-data-open/accordion-item:opacity-100 motion-safe:group-data-open/accordion-item:translate-y-0 motion-safe:group-data-open/accordion-item:blur-none motion-reduce:transition-none",
          variant === "divided" && "pb-4",
          variant === "contained" && "px-5 pb-5",
          variant === "separated" && "px-4 pb-4",
          className
        )}
      >
        {children}
      </div>
    </AccordionPrimitive.Panel>
  );
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };