Accordion
Collapsible sections for FAQs, settings and long forms.
'use client';
import { Accordion } from '@akhatmirzo/ui';
const faq = [
{
value: 'delivery',
title: 'Yetkazib berish qancha vaqt oladi?',
body: "Toshkent bo'ylab 1 kun, viloyatlarga 2–3 ish kuni.",
},
{
value: 'payment',
title: "Qanday to'lov usullari bor?",
body: "Click, Payme, Uzum va naqd pul. Karta orqali to'lov darhol tasdiqlanadi.",
},
{
value: 'return',
title: "Mahsulotni qaytarsa bo'ladimi?",
body: "Ha, 14 kun ichida, qadoq buzilmagan bo'lsa.",
},
];
export default function AccordionBasic() {
return (
<Accordion defaultValue="delivery" className="max-w-xl">
{faq.map((q) => (
<Accordion.Item key={q.value} value={q.value} title={q.title}>
{q.body}
</Accordion.Item>
))}
</Accordion>
);
}
Import
import { Accordion } from '@akhatmirzo/ui';When to use
- FAQs — users open only the question they care about, and the page stays short.
- Settings and long forms — split them into sections instead of showing everything at once.
- Don't hide essential content. Anything users must see should stay visible — most people never open collapsed text.
- If only one section should be visible at a time and the sections are equally important,
Tabsmay fit better.
Anatomy
The simplest form is an Accordion.Item with a title: the header and the content are rendered
for you, and children become the content.
<Accordion defaultValue="delivery">
<Accordion.Item value="delivery" title="How long does delivery take?">
One day within Tashkent, 2–3 business days to other regions.
</Accordion.Item>
</Accordion>To put your own content in the header (a badge, an icon), skip title and write
Accordion.Trigger + Accordion.Content yourself (see "Composition" below).
Variants
| Variant | Looks like |
|---|---|
separated | Each section is its own card, 8px apart (default) |
bordered | All sections in one card, divided by lines |
ghost | No background, just divider lines |
'use client';
import { Accordion } from '@akhatmirzo/ui';
export default function AccordionVariants() {
return (
<div className="grid w-full gap-6 md:grid-cols-3">
{(['separated', 'bordered', 'ghost'] as const).map((variant) => (
<div key={variant} className="flex flex-col gap-2">
<span className="text-xs font-medium text-text-muted">{variant}</span>
<Accordion variant={variant} defaultValue="a">
<Accordion.Item value="a" title="Yetkazib berish">
Toshkent bo'ylab 1 kun.
</Accordion.Item>
<Accordion.Item value="b" title="To'lov">
Click, Payme yoki naqd.
</Accordion.Item>
<Accordion.Item value="c" title="Qaytarish">
14 kun ichida.
</Accordion.Item>
</Accordion>
</div>
))}
</div>
);
}
Several open at once
By default (type="single"), one section is open at a time — opening another closes the
previous one. With type="multiple", several can be open together; value and defaultValue
become arrays.
'use client';
import { Accordion } from '@akhatmirzo/ui';
export default function AccordionMultiple() {
return (
<Accordion type="multiple" defaultValue={['delivery', 'payment']} className="max-w-xl">
<Accordion.Item value="delivery" title="Yetkazib berish qancha vaqt oladi?">
Toshkent bo'ylab 1 kun, viloyatlarga 2–3 ish kuni.
</Accordion.Item>
<Accordion.Item value="payment" title="Qanday to'lov usullari bor?">
Click, Payme, Uzum va naqd pul.
</Accordion.Item>
<Accordion.Item value="return" title="Mahsulotni qaytarsa bo'ladimi?">
Ha, 14 kun ichida, qadoq buzilmagan bo'lsa.
</Accordion.Item>
</Accordion>
);
}
Always one open
With type="single", clicking the open section closes it (collapsible defaults to true).
With collapsible={false}, exactly one section always stays open — it behaves like picking one
of several options.
'use client';
import { Accordion } from '@akhatmirzo/ui';
export default function AccordionNonCollapsible() {
return (
<Accordion collapsible={false} defaultValue="courier" variant="bordered" className="max-w-xl">
<Accordion.Item value="courier" title="Kuryer orqali">
Ertasi kuni eshigingizgacha. Narxi — 25 000 so'm.
</Accordion.Item>
<Accordion.Item value="pickup" title="Olib ketish punktidan">
Toshkentdagi 12 ta punktdan bepul olib ketasiz.
</Accordion.Item>
<Accordion.Item value="post" title="Pochta orqali">
Viloyatlarga 2–3 ish kuni. Narxi — 18 000 so'm.
</Accordion.Item>
</Accordion>
);
}
Composition
Inside an Accordion.Item, use Accordion.Trigger and Accordion.Content to add a badge, an
icon or a second line to the header. A single section can be turned off with disabled.
'use client';
import { Accordion, Badge } from '@akhatmirzo/ui';
export default function AccordionComposition() {
return (
<Accordion variant="bordered" className="max-w-xl">
<Accordion.Item value="notifications">
<Accordion.Trigger>
<span className="flex items-center gap-2">
Bildirishnomalar
<Badge color="primary" size="sm">
3 ta yangi
</Badge>
</span>
</Accordion.Trigger>
<Accordion.Content>
<ul className="m-0 flex list-disc flex-col gap-1 pl-5">
<li>Buyurtma #1042 yetkazildi</li>
<li>Yangi sharh qoldirildi</li>
<li>To'lov qabul qilindi</li>
</ul>
</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="security">
<Accordion.Trigger>Xavfsizlik</Accordion.Trigger>
<Accordion.Content>Parolni oxirgi marta 3 oy oldin o'zgartirgansiz.</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="billing" disabled>
<Accordion.Trigger>To'lov ma'lumotlari (tez orada)</Accordion.Trigger>
<Accordion.Content>—</Accordion.Content>
</Accordion.Item>
</Accordion>
);
}
Controlled
With value + onValueChange, you decide which sections are open — for an "Expand all" button,
syncing with the URL and so on.
'use client';
import { Accordion, Button } from '@akhatmirzo/ui';
import { useState } from 'react';
const sections = [
{ value: 'profile', title: "Shaxsiy ma'lumotlar", body: "Ism, telefon va tug'ilgan sana." },
{ value: 'address', title: 'Manzillar', body: 'Uy: Toshkent, Chilonzor tumani, 9-mavze.' },
{ value: 'cards', title: "To'lov kartalari", body: 'Uzcard •• 4521, Humo •• 9087.' },
];
export default function AccordionControlled() {
const [open, setOpen] = useState<string[]>(['profile']);
return (
<div className="flex w-full max-w-xl flex-col gap-3">
<div className="flex flex-wrap gap-2">
<Button size="sm" variant="outline" onClick={() => setOpen(sections.map((s) => s.value))}>
Hammasini ochish
</Button>
<Button size="sm" variant="outline" onClick={() => setOpen([])}>
Hammasini yopish
</Button>
</div>
<Accordion type="multiple" value={open} onValueChange={setOpen}>
{sections.map((s) => (
<Accordion.Item key={s.value} value={s.value} title={s.title}>
{s.body}
</Accordion.Item>
))}
</Accordion>
</div>
);
}
Props
Accordion
Also accepts <div> attributes.
| Prop | Type | Default | Description |
|---|---|---|---|
type | 'single' | 'multiple' | 'single' | Whether one or several sections can be open |
value | string · string[] | — | Open section(s) — an array with multiple |
defaultValue | string · string[] | — | Initially open section(s) |
onValueChange | (value: string) => void · (value: string[]) => void | — | Called when sections open or close |
collapsible | boolean | true | single only: whether the open section can be closed again |
variant | 'separated' | 'bordered' | 'ghost' | 'separated' | Visual style |
disabled | boolean | — | Disables every section |
orientation | 'horizontal' | 'vertical' | — | Direction of keyboard navigation (vertical by default) |
dir | 'ltr' | 'rtl' | — | Text direction |
Accordion.Item
| Prop | Type | Default | Description |
|---|---|---|---|
valuerequired | string | — | Section value |
title | ReactNode | — | If set, Trigger and Content are rendered for you; children become the content |
disabled | boolean | — | Disables the section |
Accordion.Trigger, Accordion.Content
Accordion.Trigger takes <button> attributes and draws a chevron on the right.
Accordion.Content takes <div> attributes and forceMount (to keep it in the DOM while
closed).
Data attributes
| Attribute | On | Value |
|---|---|---|
data-variant | root, item | separated · bordered · ghost |
data-state | item, trigger, content | open · closed |
data-disabled | item, trigger | present when disabled |
data-orientation | all parts | vertical · horizontal |
data-accordion-chevron | the chevron icon | — |
Keyboard
| Key | Action |
|---|---|
Enter / Space | Opens or closes the focused section |
↓ / ↑ | Moves to the next / previous header |
Home / End | First / last header |
Tab | Moves to the next focusable element |
Accessibility
- Structure. Each header is a
<button>inside an<h3>; the button getsaria-expandedandaria-controls, and the content is arole="region"labeled by its header througharia-labelledby. It is built on Radix Accordion. - The chevron is decorative (
aria-hidden) and rotates 180° when open. - Animation. The height animates over 200ms; with
prefers-reduced-motion, only a fade is used. - The focus ring only shows for keyboard focus.