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.jsonnpx shadcn@latest add https://neobrutalism.com/r/base/accordion.jsonyarn dlx shadcn@latest add https://neobrutalism.com/r/base/accordion.jsonbunx --bun shadcn@latest add https://neobrutalism.com/r/base/accordion.json
Cài đặt các dependency sau:
pnpm add @base-ui/reactnpm install @base-ui/reactyarn add @base-ui/reactbun add @base-ui/react
Sao chép và dán đoạn mã sau vào dự án của bạn.
"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
└── AccordionContentVí 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 Accordion và border-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-expanded và aria-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ím | Hành động |
|---|---|
Tab / Shift + Tab | Di chuyển focus giữa các trigger và ra ngoài |
Space / Enter | Mở hoặc đóng phần đang được focus |
ArrowDown / ArrowUp | Di chuyển focus đến trigger kế tiếp / trước đó |
Home / End | Nhả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.