wui
组件

折叠面板 Collapsible

按需展开一段补充内容,并保留清晰的状态与键盘交互。

基础示例

Loading…
pnpm dlx wui@latest add @wui/collapsible
components/ui/collapsible.tsx
"use client"

import * as React from "react"
import { Collapsible as CollapsiblePrimitive } from "radix-ui"
import { ChevronDownIcon } from "lucide-react"
import { motion, useReducedMotion } from "motion/react"

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

const CollapsibleContext = React.createContext(false)

export interface CollapsibleProps
  extends React.ComponentProps<typeof CollapsiblePrimitive.Root> {}

/** A disclosure region with controlled and uncontrolled open state. */
function Collapsible({
  open,
  defaultOpen = false,
  onOpenChange,
  ...props
}: CollapsibleProps) {
  const [internalOpen, setInternalOpen] = React.useState(defaultOpen)
  const resolvedOpen = open ?? internalOpen
  return (
    <CollapsibleContext.Provider value={resolvedOpen}>
      <CollapsiblePrimitive.Root
        data-slot="collapsible"
        open={resolvedOpen}
        onOpenChange={(next) => {
          if (open === undefined) setInternalOpen(next)
          onOpenChange?.(next)
        }}
        {...props}
      />
    </CollapsibleContext.Provider>
  )
}

export interface CollapsibleTriggerProps
  extends React.ComponentProps<typeof CollapsiblePrimitive.Trigger> {
  /** Show the built-in rotating chevron after the label. @default true */
  showIndicator?: boolean
}

function CollapsibleTrigger({
  className,
  children,
  showIndicator = true,
  asChild = false,
  ...props
}: CollapsibleTriggerProps) {
  return (
    <CollapsiblePrimitive.Trigger
      data-slot="collapsible-trigger"
      asChild={asChild}
      className={cn(
        "group flex w-full items-center gap-3 rounded-md px-3 py-2 text-left text-sm font-medium outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/35 disabled:pointer-events-none disabled:opacity-50",
        className
      )}
      {...props}
    >
      {asChild ? (
        children
      ) : (
        <>
          <span className="min-w-0 flex-1">{children}</span>
          {showIndicator ? (
            <ChevronDownIcon
              aria-hidden
              data-slot="collapsible-indicator"
              className="size-4 shrink-0 text-muted-foreground transition-transform duration-200 group-data-[state=open]:rotate-180 motion-reduce:transition-none"
            />
          ) : null}
        </>
      )}
    </CollapsiblePrimitive.Trigger>
  )
}

function CollapsibleContent({
  className,
  children,
  ...props
}: Omit<
  React.ComponentProps<typeof CollapsiblePrimitive.Content>,
  "asChild" | "forceMount"
>) {
  const open = React.useContext(CollapsibleContext)
  const reduceMotion = useReducedMotion()
  return (
    <CollapsiblePrimitive.Content forceMount asChild {...props}>
      <motion.div
        data-slot="collapsible-content"
        aria-hidden={!open}
        inert={!open}
        className={cn("overflow-hidden", className)}
        initial={false}
        animate={{ height: open ? "auto" : 0, opacity: open ? 1 : 0 }}
        transition={
          reduceMotion
            ? { duration: 0 }
            : { duration: 0.24, ease: [0.22, 1, 0.36, 1] }
        }
      >
        {children}
      </motion.div>
    </CollapsiblePrimitive.Content>
  )
}

export { Collapsible, CollapsibleContent, CollapsibleTrigger }

组件作用

Collapsible 用于隐藏暂时不需要的补充信息,例如高级设置、详情和次级导航。触发器始终可见,内容展开时使用短促的高度与透明度过渡;用户偏好减少动态效果时会直接切换状态。不要用它隐藏完成当前任务所必需的信息。

组件属性

PropTypeDefaultDescription
disabledboolean
asChildboolean
  • Collapsible 透传 Radix Root 的 opendefaultOpendisabledonOpenChange
  • CollapsibleTrigger 支持 asChild,并可通过 showIndicator={false} 隐藏默认箭头。
  • CollapsibleContent 负责展开动效,收起时会从键盘导航和辅助技术中移除内部内容。

事件

  • onOpenChange(open):用户点击触发器或受控状态变化时触发。
  • 触发器使用原生按钮语义,支持 Enter 与 Space,并同步 aria-expanded 和内容关联关系。

拓展使用

受控状态

Loading…

当页面其他区域需要读取或改变展开状态时,传入 openonOpenChange。如果折叠内容彼此互斥,应使用 Accordion,而不是手动同步多个 Collapsible