Bỏ qua đến nội dung

Accordion

Một bộ các phần có thể mở rộng được xếp chồng theo chiều dọc dành cho mục Câu hỏi thường gặp (FAQ), cài đặt và các trang có nhiều nội dung — với phong cách thiết kế theo trường phái tân thô mộc, kết hợp giữa đường viền và bóng đổ.

import {
  Accordion,
  AccordionContent,

Accordion nhóm các nội dung liên quan thành những phần xếp chồng theo chiều dọc và có thể mở rộng — mỗi phần một tiêu đề, một cú nhấp để hiện nội dung. Được xây dựng trên thành phần Base UI Accordion và tạo kiểu theo công thức neobrutalist: viền dày, bóng đổ đậm và kiểu chữ đậm.

Nên dùng khi:

  • Câu hỏi thường gặp — trường hợp kinh điển. Mỗi lúc chỉ mở một câu hỏi giúp các câu trả lời dễ đọc lướt.
  • Cài đặt và bộ lọc — thu gọn những nhóm ít dùng mà không phải ẩn chúng đi.
  • Trang nhiều nội dung — chi tiết sản phẩm, thông tin vận chuyển, nhật ký thay đổi.

Cài đặt

pnpm dlx shadcn@latest add https://neobrutalism.com/r/base/accordion.json
npx shadcn@latest add https://neobrutalism.com/r/base/accordion.json
yarn dlx shadcn@latest add https://neobrutalism.com/r/base/accordion.json
bunx --bun shadcn@latest add https://neobrutalism.com/r/base/accordion.json

Cài đặt các dependency sau:

pnpm add @base-ui/react
npm install @base-ui/react
yarn add @base-ui/react
bun add @base-ui/react

Sao chép và dán đoạn mã sau vào dự án của bạn.

components/ui/accordion.tsx
"use client"

import * as React from "react"
import { ChevronDownIcon } from "lucide-react"
import { Accordion as AccordionPrimitive } from "radix-ui"

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

// Smooth, premium easing for the open/close — fast out of the gate, gentle
// settle. Shared by the panel height and the chevron so they move in lockstep.
const EASE = "ease-[cubic-bezier(0.32,0.72,0,1)]"

function Accordion({
  className,
  ...props
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
  return (
    <AccordionPrimitive.Root
      data-slot="accordion"
      className={cn("flex w-full flex-col gap-3", className)}
      {...props}
    />
  )
}

function AccordionItem({
  className,
  ...props
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
  return (
    <AccordionPrimitive.Item
      data-slot="accordion-item"
      className={cn(
        "overflow-hidden rounded border-2 bg-background text-foreground shadow-md transition-shadow duration-200 hover:shadow-sm data-[state=open]:shadow-sm",
        className
      )}
      {...props}
    />
  )
}

function AccordionTrigger({
  className,
  children,
  ...props
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
  return (
    <AccordionPrimitive.Header data-slot="accordion-header" className="flex">
      <AccordionPrimitive.Trigger
        data-slot="accordion-trigger"
        className={cn(
          "flex flex-1 cursor-pointer items-center justify-between gap-4 px-4 py-3 text-left font-head transition-colors hover:bg-muted/50 data-[state=open]:bg-muted/40 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary [&[data-state=open]>svg]:rotate-180",
          className
        )}
        {...props}
      >
        {children}
        <ChevronDownIcon
          aria-hidden
          data-slot="accordion-trigger-icon"
          className={cn(
            "h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-300",
            EASE
          )}
        />
      </AccordionPrimitive.Trigger>
    </AccordionPrimitive.Header>
  )
}

function AccordionContent({
  className,
  children,
  ...props
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
  return (
    <AccordionPrimitive.Content
      data-slot="accordion-content"
      // Radix publishes the measured height as `--radix-accordion-content-height`
      // and toggles `data-state`. The accordion-down/up keyframes (in
      // shadcn-tailwind.css) interpolate height between that var and 0 for a real
      // slide open/close — the Base UI variant achieves the same via transitions.
      className="group/panel overflow-hidden bg-card font-body text-sm text-muted-foreground data-[state=open]:animate-accordion-down data-[state=closed]:animate-accordion-up"
      {...props}
    >
      <div
        className={cn(
          "px-4 pt-2 pb-4 transition-[opacity,transform] duration-300 ease-out",
          // Fade + nudge the content as the panel opens/closes, synced to the slide.
          "group-data-[state=closed]/panel:-translate-y-1 group-data-[state=closed]/panel:opacity-0",
          "[&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
          className
        )}
      >
        {children}
      </div>
    </AccordionPrimitive.Content>
  )
}

export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }

Cập nhật các đường dẫn import cho khớp với cấu trúc dự án của bạn.

Cách dùng

import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion"
<Accordion defaultValue={["item-1"]}>
  <AccordionItem value="item-1">
    <AccordionTrigger>Is it accessible?</AccordionTrigger>
    <AccordionContent>
      Yes. It adheres to the WAI-ARIA design pattern.
    </AccordionContent>
  </AccordionItem>
</Accordion>

Kết hợp

Dùng cách kết hợp sau để dựng một Accordion:

Accordion
├── AccordionItem
│   ├── AccordionTrigger
│   └── AccordionContent
└── AccordionItem
    ├── AccordionTrigger
    └── AccordionContent

Ví dụ

Cơ bản

Một accordion cơ bản hiển thị một mục tại một thời điểm. Mục đầu tiên mở theo mặc định.

import {
  Accordion,
  AccordionContent,

Nhiều mục

Dùng prop multiple để cho phép nhiều mục mở cùng lúc.

import {
  Accordion,
  AccordionContent,

Vô hiệu hóa

Dùng prop disabled trên AccordionItem để vô hiệu hóa từng mục riêng lẻ.

import {
  Accordion,
  AccordionContent,

Viền

Thêm border vào Accordionborder-b last:border-b-0 vào AccordionItem để thêm viền cho các mục.

import {
  Accordion,
  AccordionContent,

Card

Bọc Accordion trong một thành phần Card.

import {
  Accordion,
  AccordionContent,

RTL

Để bật hỗ trợ RTL trong Neobrutalism, xem hướng dẫn cấu hình RTL.

"use client"

import * as React from "react"

Khả năng truy cập

Accordion tuân theo mẫu Accordion của WAI-ARIA: các trigger là nút thật, được nối với aria-expandedaria-controls, nên trình đọc màn hình đọc lên từng phần cùng trạng thái của nó.

Tương tác bàn phím:

PhímHành động
Tab / Shift + TabDi chuyển focus giữa các trigger và ra ngoài
Space / EnterMở hoặc đóng phần đang được focus
ArrowDown / ArrowUpDi chuyển focus đến trigger kế tiếp / trước đó
Home / EndNhảy đến trigger đầu tiên / cuối cùng

Tham chiếu API

Xem tài liệu Base UI để biết thêm thông tin.