wui
组件

手风琴 Accordion

用于在有限空间内组织多段相关信息的互斥或多开可折叠面板。

第三方依赖 · radix-ui第三方依赖 · class-variance-authority第三方依赖 · lucide-react

基础示例 · Basic example

Loading…
pnpm dlx @wui-design/cli@latest add @wui/accordion
components/ui/accordion.tsx
"use client"

import * as React from "react"
import { Accordion as AccordionPrimitive } from "radix-ui"
import { cva, type VariantProps } from "class-variance-authority"
import { ChevronDownIcon } from "lucide-react"

import { cn } from "@/lib/utils"

const accordionVariants = cva("w-full", {
  variants: {
    variant: {
      default: "divide-y divide-border border-y border-border",
      bordered: "divide-y divide-border rounded-lg border border-border overflow-hidden bg-card",
      separated: "flex flex-col gap-2.5",
    },
  },
  defaultVariants: {
    variant: "default",
  },
})

const AccordionContext = React.createContext<{
  variant?: "default" | "bordered" | "separated"
}>({ variant: "default" })

export type AccordionProps = React.ComponentProps<typeof AccordionPrimitive.Root> &
  VariantProps<typeof accordionVariants> & {
    /** 手风琴的外观样式变体。 @default "default" */
    variant?: "default" | "bordered" | "separated"
    /** 是否禁用整个手风琴组件的所有交互。 */
    disabled?: boolean
    /** 是否将属性传递给子元素渲染。 */
    asChild?: boolean
  }

/** 手风琴折叠面板,支持单项或多项内容的高效折叠与展开。 */
function Accordion({
  className,
  variant = "default",
  children,
  ...props
}: AccordionProps) {
  return (
    <AccordionContext.Provider value={{ variant }}>
      <AccordionPrimitive.Root
        data-slot="accordion"
        data-variant={variant}
        className={cn(accordionVariants({ variant }), className)}
        {...(props as any)}
      >
        {children}
      </AccordionPrimitive.Root>
    </AccordionContext.Provider>
  )
}

const accordionItemVariants = cva("transition-colors", {
  variants: {
    variant: {
      default: "",
      bordered: "",
      separated:
        "overflow-hidden rounded-lg border border-border bg-card shadow-xs transition-[border-color] duration-200 data-[state=open]:border-foreground/20",
    },
  },
  defaultVariants: {
    variant: "default",
  },
})

export interface AccordionItemProps
  extends React.ComponentProps<typeof AccordionPrimitive.Item> {}

function AccordionItem({
  className,
  value,
  children,
  ...props
}: AccordionItemProps) {
  const { variant } = React.useContext(AccordionContext)
  return (
    <AccordionPrimitive.Item
      data-slot="accordion-item"
      value={value}
      className={cn(accordionItemVariants({ variant }), className)}
      {...props}
    >
      {children}
    </AccordionPrimitive.Item>
  )
}

export interface AccordionTriggerProps
  extends React.ComponentProps<typeof AccordionPrimitive.Trigger> {
  /** 是否展示右侧旋转指示箭头。 @default true */
  showIndicator?: boolean
}

function AccordionTrigger({
  className,
  children,
  showIndicator = true,
  ...props
}: AccordionTriggerProps) {
  return (
    <AccordionPrimitive.Header className="flex">
      <AccordionPrimitive.Trigger
        data-slot="accordion-trigger"
        className={cn(
          "group flex flex-1 cursor-pointer select-none items-center justify-between gap-3 px-4 py-3.5 text-left text-sm font-medium text-foreground outline-none transition-colors duration-200 hover:bg-muted/40 focus-visible:ring-[3px] focus-visible:ring-inset focus-visible:ring-ring/35 disabled:pointer-events-none disabled:opacity-50",
          className
        )}
        {...props}
      >
        <span className="flex-1 min-w-0">{children}</span>
        {showIndicator && (
          <ChevronDownIcon
            aria-hidden
            data-slot="accordion-indicator"
            className="size-4 shrink-0 text-muted-foreground transition-transform duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] group-hover:text-foreground group-data-[state=open]:rotate-180 motion-reduce:transition-none"
          />
        )}
      </AccordionPrimitive.Trigger>
    </AccordionPrimitive.Header>
  )
}

export interface AccordionContentProps
  extends React.ComponentProps<typeof AccordionPrimitive.Content> {}

function AccordionContent({
  className,
  children,
  ...props
}: AccordionContentProps) {
  return (
    <AccordionPrimitive.Content
      data-slot="accordion-content"
      className={cn(
        "overflow-hidden text-sm leading-relaxed text-muted-foreground duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down motion-reduce:animate-none",
        className
      )}
      {...props}
    >
      <div className="px-4 pb-4 pt-0 duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:animate-none [[data-state=closed]>&]:animate-out [[data-state=closed]>&]:fade-out-0 [[data-state=open]>&]:animate-in [[data-state=open]>&]:fade-in-0 [[data-state=open]>&]:slide-in-from-top-1">
        {children}
      </div>
    </AccordionPrimitive.Content>
  )
}

export {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
  accordionVariants,
}

组件作用 · What it's for

Accordion 适合将大量分步信息、常见问题解答(FAQ)、配置项分组或长篇说明以折叠列表形式组织。

适用场景

  • 常见问题解答 (FAQ):提供紧凑整洁的折叠面板,点击标题展开详细解答。
  • 复杂表单与配置分组:在设置页按模块折叠次要配置项。
  • 单选互斥与多开支持:通过 type="single" 或 type="multiple" 自由控制展开行为。

何时不建议使用

  • 如果只有单一独立的展开/折叠块,推荐直接使用更轻量的 Collapsible。

组件属性 · Props

属性类型默认值说明
disabledboolean—Whether or not an accordion is disabled from user interaction. 是否禁用整个手风琴组件的所有交互。 @defaultValue false
asChildboolean—是否将属性传递给子元素渲染。
variant"default" | "bordered" | "separated"default手风琴的外观样式变体。

事件 · Events

  • onValueChange?: (value: string | string[]) => void:当展开项发生改变时触发的回调。

拓展使用 · Extended usage

支持通过 variant="default" | "bordered" | "separated" 切换通栏分割、圆角外框以及独立卡片式 3 种视觉形态(见基础示例中的切换)。

动效说明

展开与收起时,面板高度基于 Radix 测得的 --radix-accordion-content-height 以 300ms 缓出曲线过渡,正文同步淡入并轻微下移,指示箭头平滑旋转;separated 形态下展开项的边框会加深以强调当前焦点。系统开启“减少动态效果”时,所有过渡自动关闭。

多项展开与受控

type="multiple" 允许同时展开多项。配合受控的 value / onValueChange,可以实现“全部展开 / 全部收起”等批量操作:

Loading…