Accordion
Stos nagłówków, z których każdy odsłania sekcję treści — stopniowe ujawnianie informacji drugorzędnych.
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion"
export default function AccordionDemo() {
return (
<Accordion defaultValue={["shipping"]} className="w-full max-w-sm">
<AccordionItem value="shipping">
<AccordionTrigger>Ile trwa wysyłka?</AccordionTrigger>
<AccordionContent>
Zamówienia wysyłamy w ciągu jednego dnia roboczego. Dostawa trwa 2–4 dni w UE i 5–8 dni w pozostałych krajach.
</AccordionContent>
</AccordionItem>
<AccordionItem value="returns">
<AccordionTrigger>Czy mogę zwrócić produkt?</AccordionTrigger>
<AccordionContent>
Tak — w ciągu 30 dni, jeśli produkt jest w oryginalnym stanie. Dla członków klubu zwroty są bezpłatne.
</AccordionContent>
</AccordionItem>
<AccordionItem value="warranty">
<AccordionTrigger>Czy produkty mają gwarancję?</AccordionTrigger>
<AccordionContent>
Każdy produkt jest objęty dwuletnią gwarancją na wady fabryczne.
</AccordionContent>
</AccordionItem>
</Accordion>
)
}Anatomia
- 1WyzwalaczPrzycisk w nagłówku, który nazywa sekcję. Przełącza ją kliknięcie w dowolnym miejscu wiersza.
- 2ElementJedna sekcja: wyzwalacz i jego panel. Elementy są rozdzielone cienkimi liniami.
- 3IkonaWskazuje w dół, gdy sekcja jest zamknięta, a po jej otwarciu odwraca się.
- 4PanelTreść sekcji. Przy otwieraniu i zamykaniu animuje swoją wysokość, a zamknięty jest pomijany przy przechodzeniu klawiszem Tab.
Instalacja
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/accordion.jsonUżycie
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion"<Accordion defaultValue={["shipping"]}>
<AccordionItem value="shipping">
<AccordionTrigger>How long does shipping take?</AccordionTrigger>
<AccordionContent>Orders ship within one business day.</AccordionContent>
</AccordionItem>
</Accordion>defaultValue i value to zawsze tablice wartości elementów — nawet wtedy, gdy otwarty może być tylko jeden element. Nadaj każdemu elementowi stałą wartość value, żeby jego stan przetrwał ponowne renderowanie i dało się nim sterować.
Przykłady
FAQ
Klasyczne zastosowanie: wiele pytań, z których każdego czytelnika dotyczy tylko kilka. Domyślnie otwarty jest jeden element naraz, więc lista pozostaje krótka i łatwa do przejrzenia, gdy ktoś przechodzi od jednej odpowiedzi do drugiej.
Najczęściej zadawane pytania
Wszystko o planach, rozliczeniach i twoich danych.
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion"
const questions = [
{
value: "seats",
question: "Jak rozliczane są licencje?",
answer:
"Płacisz tylko za aktywnych członków. Za osobę, która nie zalogowała się w danym okresie rozliczeniowym, nie pobieramy opłaty, a licencje dla osób dodanych w trakcie cyklu rozliczamy proporcjonalnie co do dnia.",
},
{
value: "switch",
question: "Czy mogę później zmienić plan?",
answer:
"W każdej chwili. Przejście na wyższy plan działa od razu, a na niższy — od końca bieżącego okresu rozliczeniowego, więc nie tracisz dostępu do opłaconych funkcji.",
},
{
value: "trial",
question: "Co się stanie po zakończeniu okresu próbnego?",
answer:
"Twoja przestrzeń robocza przechodzi na plan Free. Nic nie zostanie usunięte — projekty ponad limity planu Free będą dostępne tylko do odczytu, dopóki nie przejdziesz na wyższy plan albo ich nie zarchiwizujesz.",
},
{
value: "invoices",
question: "Czy wystawiacie faktury VAT?",
answer:
"Tak. Dodaj dane firmy i numer NIP w sekcji Rozliczenia, a pojawią się na każdej fakturze. Faktury są dostępne w formacie PDF i możemy je wysyłać na osobny adres e-mail do rozliczeń.",
},
{
value: "security",
question: "Gdzie przechowywane są moje dane?",
answer:
"Domyślnie w UE, zaszyfrowane zarówno w spoczynku, jak i podczas przesyłania. Przestrzenie robocze w planie Enterprise mogą wybrać region danych i używać własnych kluczy szyfrowania.",
},
]
export default function AccordionFaq() {
return (
<div className="flex w-full max-w-xl flex-col gap-6">
<div className="flex flex-col gap-1.5">
<h3 className="text-heading-md text-foreground">
Najczęściej zadawane pytania
</h3>
<p className="text-sm text-muted-foreground">
Wszystko o planach, rozliczeniach i twoich danych.
</p>
</div>
<Accordion>
{questions.map((item) => (
<AccordionItem key={item.value} value={item.value}>
<AccordionTrigger>{item.question}</AccordionTrigger>
<AccordionContent>{item.answer}</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
)
}Wiele otwartych sekcji
Ustaw multiple, gdy trzeba porównywać sekcje albo pracować w kilku naraz — przy filtrach, ustawieniach czy pogrupowanych opcjach formularza. Tutaj dwie grupy filtrów są otwarte od początku.
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion"
import { Checkbox } from "@/components/ui/checkbox"
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"
const filters = [
{
value: "status",
label: "Status",
options: ["Backlog", "W toku", "W przeglądzie", "Gotowe"],
},
{
value: "priority",
label: "Priorytet",
options: ["Pilny", "Wysoki", "Średni", "Niski"],
},
{
value: "team",
label: "Zespół",
options: ["Projektowanie", "Inżynieria", "Marketing"],
},
]
export default function AccordionMultiple() {
return (
<Accordion
multiple
defaultValue={["status", "priority"]}
className="w-full max-w-64"
>
{filters.map((filter) => (
<AccordionItem key={filter.value} value={filter.value}>
<AccordionTrigger>{filter.label}</AccordionTrigger>
<AccordionContent>
{/* pt-1 keeps the first checkbox's 24px target clear of the trigger. */}
<FieldGroup className="gap-2.5 pt-1">
{filter.options.map((option, index) => {
const id = `${filter.value}-${index}`
return (
<Field key={id} orientation="horizontal">
<Checkbox id={id} defaultChecked={index === 1} />
<FieldLabel htmlFor={id} className="font-normal">
{option}
</FieldLabel>
</Field>
)
})}
</FieldGroup>
</AccordionContent>
</AccordionItem>
))}
</Accordion>
)
}Z ikonami i podsumowaniami
Wyzwalacze przyjmują dowolne elementy liniowe. Ikona na początku i jednowierszowe podsumowanie pozwalają ocenić, czy warto otworzyć sekcję — bez jej otwierania. Dodaj wcięcie panelu (className w AccordionContent stylizuje jego wewnętrzny kontener), żeby tekst panelu był wyrównany do tytułu.
import {
KeyRoundIcon,
MonitorSmartphoneIcon,
ShieldCheckIcon,
} from "lucide-react"
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion"
const sections = [
{
value: "2fa",
icon: ShieldCheckIcon,
title: "Uwierzytelnianie dwuskładnikowe",
summary: "Włączone · Aplikacja uwierzytelniająca",
body: "Przy logowaniu na nowym urządzeniu poprosimy o kod z aplikacji uwierzytelniającej. Przechowuj kody odzyskiwania w bezpiecznym miejscu — jeśli zgubisz telefon, tylko one pozwolą ci odzyskać dostęp.",
},
{
value: "passkeys",
icon: KeyRoundIcon,
title: "Klucze dostępu",
summary: "2 klucze dostępu",
body: "Klucze dostępu pozwalają logować się za pomocą Face ID, Touch ID lub klucza bezpieczeństwa zamiast hasła. Są odporne na phishing i przechowywane na twoich urządzeniach.",
},
{
value: "sessions",
icon: MonitorSmartphoneIcon,
title: "Aktywne sesje",
summary: "3 urządzenia",
body: "MacBook Pro · Warszawa — bieżąca sesja. iPhone 16 · Warszawa — 2 godz. temu. Chrome w systemie Windows · Berlin — 3 dni temu.",
},
]
export default function AccordionWithIcons() {
return (
<Accordion
defaultValue={["2fa"]}
className="w-full max-w-md rounded-xl border bg-card px-4 shadow-raised"
>
{sections.map((section) => (
<AccordionItem key={section.value} value={section.value}>
<AccordionTrigger>
<span className="flex items-center gap-3">
<span className="flex size-8 shrink-0 items-center justify-center rounded-lg border bg-surface text-muted-foreground">
<section.icon className="size-4" aria-hidden="true" />
</span>
<span className="flex flex-col gap-0.5">
<span>{section.title}</span>
<span className="text-body-xs text-muted-foreground">
{section.summary}
</span>
</span>
</span>
</AccordionTrigger>
<AccordionContent className="pl-11">{section.body}</AccordionContent>
</AccordionItem>
))}
</Accordion>
)
}Wyłączony element
Wyłącz element, który istnieje, ale jest niedostępny, i wyjaśnij powód w jego etykiecie. Wyzwalacz pozostaje widoczny, czytniki ekranu odczytują go jako wyłączony i nie da się go przełączyć.
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion"
export default function AccordionDisabled() {
return (
<Accordion className="w-full max-w-sm">
<AccordionItem value="profile">
<AccordionTrigger>Profil</AccordionTrigger>
<AccordionContent>
Twoje imię i nazwisko, zdjęcie oraz opis widzą wszyscy w przestrzeni roboczej.
</AccordionContent>
</AccordionItem>
<AccordionItem value="sso" disabled>
<AccordionTrigger>Logowanie jednokrotne (Enterprise)</AccordionTrigger>
<AccordionContent>
Połącz z Okta, Entra ID lub dowolnym dostawcą SAML 2.0.
</AccordionContent>
</AccordionItem>
<AccordionItem value="notifications">
<AccordionTrigger>Powiadomienia</AccordionTrigger>
<AccordionContent>
Wybierz, o jakiej aktywności chcesz dostawać e-maile lub powiadomienia push.
</AccordionContent>
</AccordionItem>
</Accordion>
)
}Tryb kontrolowany
Kontroluj value, aby otwierać sekcje z zewnątrz — na potrzeby przycisku Rozwiń wszystkie, linków prowadzących prosto do sekcji czy przywracania tego, co ktoś miał wcześniej otwarte. onValueChange otrzymuje nową tablicę wartości otwartych elementów.
prfct do zmiennej PATH."use client"
import * as React from "react"
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion"
import { Button } from "@/components/ui/button"
const code =
"rounded-md border bg-surface px-1 py-0.5 font-mono text-xs text-foreground"
const steps = [
{
value: "install",
title: "Zainstaluj CLI",
body: (
<>
Uruchom instalator raz na każdym komputerze — doda polecenie{" "}
<code className={code}>prfct</code> do zmiennej PATH.
</>
),
},
{
value: "link",
title: "Połącz projekt",
body: (
<>
W katalogu repozytorium uruchom <code className={code}>prfct link</code> i wybierz przestrzeń roboczą, z którą chcesz się połączyć.
</>
),
},
{
value: "deploy",
title: "Wdróż",
body: "Wypchnij zmiany do gałęzi main. Każdy commit dostaje adres podglądu, a main trafia na produkcję.",
},
]
export default function AccordionControlled() {
const [value, setValue] = React.useState<string[]>(["install"])
const allOpen = value.length === steps.length
return (
<div className="flex w-full max-w-md flex-col gap-3">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">
{`Rozwinięte: ${value.length} z ${steps.length}`}
</span>
<Button
variant="ghost"
size="xs"
onClick={() => setValue(allOpen ? [] : steps.map((s) => s.value))}
>
{allOpen ? "Zwiń wszystkie" : "Rozwiń wszystkie"}
</Button>
</div>
<Accordion multiple value={value} onValueChange={setValue}>
{steps.map((step) => (
<AccordionItem key={step.value} value={step.value}>
<AccordionTrigger>{step.title}</AccordionTrigger>
<AccordionContent>{step.body}</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
)
}const [value, setValue] = React.useState<string[]>(["install"])
<Accordion multiple value={value} onValueChange={setValue}>
…
</Accordion>Ruch
Panele animują swoją rzeczywistą wysokość z czasem trwania base i krzywą przejścia standard, a ich treść płynnie się pojawia w tym samym rytmie; strzałka obraca się synchronicznie. Nic nie jest skalowane ani przycinane, więc tekst nigdy się nie rozmywa. Przy prefers-reduced-motion sekcje otwierają się natychmiast.
Wytyczne
Kiedy używać
- Aby skrócić długie strony, których sekcje dotyczą tylko części czytelników: FAQ, szczegóły produktu, informacje o wersji.
- Aby pogrupować powiązane ustawienia lub filtry, do których wraca się wybiórczo.
- Na małych ekranach — żeby treść drugorzędna była w zasięgu jednego dotknięcia, zamiast zajmować miejsce, które trzeba przewinąć.
Kiedy nie używać
- Do treści, której potrzebuje każdy. Ukrycie jej kosztuje dodatkowe kliknięcie i utrudnia jej znalezienie — po prostu ją pokaż.
- Do pojedynczego obszaru, który można pokazać lub ukryć — użyj komponentu Collapsible.
- Do przełączania między widokami tych samych danych — użyj komponentu Tabs.
- Do nawigacji — użyj komponentu Sidebar lub Navigation Menu.
Pisz wyzwalacze łatwe do przejrzenia
Wyzwalacze są nagłówkami. Pisz je krótko, zaczynaj od słowa kluczowego i dbaj, żeby ich zakresy się nie pokrywały — wtedy od razu widać, pod którym kryje się odpowiedź. W FAQ formułuj je jako pytania, które zadałby czytelnik.
Zachowaj płaską strukturę
Nie zagnieżdżaj akordeonów. Drugi poziom ukrywa treść za dwoma kliknięciami i utrudnia śledzenie, co jest otwarte, a co zamknięte. Jeśli treść wymaga hierarchii, daj jej osobną stronę.
Nie ukrywaj tego, co kluczowe dla zadania
Ceny, błędy, pola wymagane i wszystko, co może zmienić decyzję, muszą pozostać widoczne. Akordeon służy do treści pomocniczych — jeśli po zwinięciu sekcji ktoś mógłby przeoczyć coś ważnego, ta treść nie powinna się w nim znaleźć.
Dostępność
Każdy wyzwalacz to <button> wewnątrz <h3>, z atrybutem aria-expanded oraz — gdy sekcja jest otwarta — aria-controls, który wskazuje jego panel. Panele mają role="region", a ich etykietą jest wyzwalacz, więc czytnik ekranu informuje, w której sekcji znajduje się użytkownik.
| Klawisz | Działanie |
|---|---|
Tab | Przenosi fokus na następny wyzwalacz albo do elementów otwartego panelu, które mogą przyjąć fokus. |
ShiftTab | Przenosi fokus na poprzedni wyzwalacz lub poprzedni element, który może przyjąć fokus. |
EnterSpacja | Rozwija lub zwija sekcję, na której jest fokus. |
- Bez nawigacji strzałkami. Zgodnie z aktualizacją WAI-ARIA Authoring Practices z 2025 roku nagłówki akordeonu są zwykłymi elementami kolejności tabulacji; komponent nie przechwytuje klawiszy strzałek.
- Poziom nagłówka. Wyzwalacze są umieszczone w
<h3>. Jeśli to zaburza strukturę nagłówków twojej strony, zmieńrenderelementuAccordionPrimitive.Headerw swojej kopii komponentu (na przykładrender={<h2 />}). - Wyszukiwanie na stronie. Ustaw
hiddenUntilFoundna elemencie głównym (lub na panelu), a wbudowane wyszukiwanie przeglądarki przeszuka zamknięte sekcje i otworzy tę, w której znajdzie dopasowanie. - Wyłączone elementy pozostają w kolejności odczytu i są ogłaszane jako wyłączone, więc użytkownik wie, że taka sekcja istnieje.
Dokumentacja API
Accordion
Grupuje elementy i zarządza tym, które z nich są otwarte. Renderuje <div>. Przyjmuje wszystkie propsy Accordion.Root z Base UI.
AccordionItem
Jeden nagłówek i jego panel. Renderuje <div>.
AccordionTrigger
Przycisk, który przełącza swój element, opakowany w <h3>. Zawiera obracającą się strzałkę. Przyjmuje wszystkie propsy Accordion.Trigger z Base UI.
AccordionContent
Zwijany panel. className to ciąg znaków, który styluje treść z paddingiem wewnątrz panelu (padding, typografia), a zewnętrzny panel odpowiada za animację wysokości. Przyjmuje wszystkie pozostałe propsy Accordion.Panel z Base UI, w tym hiddenUntilFound i keepMounted.