Przejdź do treści

Collapsible

Pojedynczy obszar, który można pokazać lub ukryć — najmniejsza jednostka stopniowego ujawniania treści.

Kod źródłowyBase UIRejestr
components/ui · 4 pliki
accordion.tsx
import { ChevronsUpDownIcon, FileCodeIcon } from "lucide-react"

import { Button } from "@/components/ui/button"
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible"

const files = ["button.tsx", "dialog.tsx", "select.tsx"]

function FileRow({ name }: { name: string }) {
  return (
    <div className="flex h-9 items-center gap-2.5 rounded-lg border bg-card px-3 font-mono text-code text-foreground shadow-raised">
      <FileCodeIcon
        className="size-4 text-muted-foreground"
        aria-hidden="true"
      />
      {name}
    </div>
  )
}

export default function CollapsibleDemo() {
  return (
    <Collapsible className="flex w-full max-w-72 flex-col gap-2">
      <div className="flex items-center justify-between gap-4 pl-1">
        <span className="text-sm font-semibold text-foreground">
          components/ui{" "}
          <span className="font-normal text-muted-foreground">· 4 pliki</span>
        </span>
        <CollapsibleTrigger
          render={
            <Button variant="ghost" size="icon-sm" aria-label="Pokaż lub ukryj pliki" />
          }
        >
          <ChevronsUpDownIcon />
        </CollapsibleTrigger>
      </div>
      <FileRow name="accordion.tsx" />
      <CollapsibleContent className="flex flex-col gap-2">
        {files.map((file) => (
          <FileRow key={file} name={file} />
        ))}
      </CollapsibleContent>
    </Collapsible>
  )
}

Anatomia

Zmieniono 3 pliki
button.tsx
dialog.tsx
select.tsx
  1. 1WyzwalaczPrzycisk, który pokazuje i ukrywa treść. Ma atrybut aria-expanded.
  2. 2TreśćObszar, który się otwiera i zamyka. Jego wysokość jest animowana, a po zamknięciu jest ukryty dla wszystkich.

Instalacja

$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/collapsible.json

Użycie

import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible"
<Collapsible>
  <CollapsibleTrigger render={<Button variant="ghost" size="sm" />}>
    Show details
  </CollapsibleTrigger>
  <CollapsibleContent>Anything you want to reveal.</CollapsibleContent>
</Collapsible>

CollapsibleTrigger renderuje <button> bez stylów. Żeby nadać mu wygląd, przekaż przez render Button z prfct — zachowa on własne style i zyska działanie oraz atrybuty ARIA wyzwalacza.

Przykłady

Pokaż więcej

Odsłoń resztę długiego podsumowania. Kontroluj open, gdy etykieta wyzwalacza ma się zmieniać razem ze stanem — Pokaż wszystkie zmiany zmienia się w Pokaż mniej.

Wydanie 2.4 · 18 września

Szybsze kompilacje, spokojniejsze logi

Kompilacje przyrostowe korzystają teraz z pamięci podręcznej między gałęziami, co skraca medianę czasu kompilacji o 38%. Logi grupują powtarzające się ostrzeżenia, zamiast wypisywać każde z osobna.

"use client"

import * as React from "react"
import { ChevronDownIcon } from "lucide-react"

import { Button } from "@/components/ui/button"
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible"

export default function CollapsibleShowMore() {
  const [open, setOpen] = React.useState(false)

  return (
    <Collapsible
      open={open}
      onOpenChange={setOpen}
      className="flex w-full max-w-md flex-col gap-3 rounded-xl border bg-card p-5 shadow-raised"
    >
      <div className="flex flex-col gap-1">
        <span className="text-xs font-medium text-muted-foreground">
          Wydanie 2.4 · 18 września
        </span>
        <h3 className="text-heading-sm text-foreground">
          Szybsze kompilacje, spokojniejsze logi
        </h3>
      </div>
      <p className="text-sm leading-[1.375rem] text-muted-foreground">
        Kompilacje przyrostowe korzystają teraz z pamięci podręcznej między gałęziami, co skraca medianę czasu kompilacji o 38%. Logi grupują powtarzające się ostrzeżenia, zamiast wypisywać każde z osobna.
      </p>
      <CollapsibleContent>
        <ul className="flex list-disc flex-col gap-1.5 pl-5 text-sm leading-[1.375rem] text-muted-foreground marker:text-gray-8">
          <li>Wdrożenia podglądowe przy zimnym starcie uruchamiają się 2× szybciej.</li>
          <li>CLI wyświetla tabelę podsumowania po każdym wdrożeniu.</li>
          <li>Zmienne środowiskowe można ograniczyć do jednej gałęzi.</li>
          <li>Naprawiono rzadką awarię przy anulowaniu kompilacji w trakcie przesyłania.</li>
        </ul>
      </CollapsibleContent>
      <CollapsibleTrigger
        render={<Button variant="link" size="sm" className="w-fit" />}
      >
        {open ? "Pokaż mniej" : "Pokaż wszystkie zmiany"}
        <ChevronDownIcon
          data-icon="inline-end"
          className="transition-transform duration-base ease-standard in-data-panel-open:rotate-180"
        />
      </CollapsibleTrigger>
    </Collapsible>
  )
}

Drzewo plików

Komponenty Collapsible bez problemu się zagnieżdżają, dlatego są podstawą drzew. Każdy folder to osobny Collapsible; strzałka odczytuje atrybut data-panel-open swojego wyzwalacza, więc każdy poziom obraca się niezależnie.

layout.tsx
page.tsx
package.json
import { ChevronRightIcon, FileIcon, FolderIcon } from "lucide-react"

import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible"

type Node = { name: string; children?: Node[] }

const tree: Node[] = [
  {
    name: "app",
    children: [
      { name: "layout.tsx" },
      { name: "page.tsx" },
      {
        name: "docs",
        children: [{ name: "layout.tsx" }, { name: "page.tsx" }],
      },
    ],
  },
  {
    name: "components",
    children: [
      { name: "ui", children: [{ name: "button.tsx" }, { name: "card.tsx" }] },
    ],
  },
  { name: "package.json" },
]

function TreeNode({ node, depth }: { node: Node; depth: number }) {
  const indent = { paddingLeft: `${depth * 1.25 + 0.5}rem` }

  if (!node.children) {
    return (
      <div
        className="flex h-8 items-center gap-2 rounded-md pr-2 text-sm text-foreground"
        style={indent}
      >
        <span className="size-3.5" aria-hidden="true" />
        <FileIcon className="size-4 text-muted-foreground" aria-hidden="true" />
        {node.name}
      </div>
    )
  }

  return (
    <Collapsible defaultOpen={depth === 0}>
      <CollapsibleTrigger
        className="flex h-8 w-full items-center gap-2 rounded-md pr-2 text-left text-sm text-foreground focus-ring-inset hover:bg-accent"
        style={indent}
      >
        <ChevronRightIcon
          className="size-3.5 text-muted-foreground transition-transform duration-base ease-standard in-data-panel-open:rotate-90"
          aria-hidden="true"
        />
        <FolderIcon
          className="size-4 text-muted-foreground"
          aria-hidden="true"
        />
        {node.name}
      </CollapsibleTrigger>
      <CollapsibleContent>
        {node.children.map((child) => (
          <TreeNode key={child.name} node={child} depth={depth + 1} />
        ))}
      </CollapsibleContent>
    </Collapsible>
  )
}

export default function CollapsibleFileTree() {
  return (
    <div className="w-full max-w-64 rounded-xl border bg-card p-1.5 shadow-raised">
      {tree.map((node) => (
        <TreeNode key={node.name} node={node} depth={0} />
      ))}
    </div>
  )
}
Drzewo to więcej niż rozwijanie
Po drzewie plików zbudowanym z komponentów Collapsible można poruszać się klawiszem Tab. Pełne drzewo ARIA (role="tree" z nawigacją strzałkami i wyszukiwaniem przez wpisywanie) to inny wzorzec — sięgnij po nie, gdy użytkownicy przeglądają setki węzłów.

Ustawienia zaawansowane

Schowaj rzadko zmieniane opcje pod najważniejszymi polami formularza. Pola pozostają w kolejności DOM dokładnie tam, gdzie mają zastosowanie, więc formularz nadal czyta się od góry do dołu.

import { ChevronDownIcon } from "lucide-react"

import { Button } from "@/components/ui/button"
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible"
import {
  Field,
  FieldDescription,
  FieldGroup,
  FieldLabel,
} from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Switch } from "@/components/ui/switch"

export default function CollapsibleAdvancedSettings() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-5">
      <FieldGroup>
        <Field>
          <FieldLabel htmlFor="webhook-url">Adres URL punktu końcowego</FieldLabel>
          <Input
            id="webhook-url"
            defaultValue="https://api.example.com/hooks"
          />
        </Field>
      </FieldGroup>
      <Collapsible className="group/advanced flex flex-col gap-4">
        <CollapsibleTrigger
          render={<Button variant="ghost" size="sm" className="-ml-3 w-fit" />}
        >
          Ustawienia zaawansowane
          <ChevronDownIcon
            data-icon="inline-end"
            className="transition-transform duration-base ease-standard group-data-open/advanced:rotate-180"
          />
        </CollapsibleTrigger>
        <CollapsibleContent>
          <FieldGroup className="rounded-xl border bg-surface p-4">
            <Field>
              <FieldLabel htmlFor="webhook-secret">Klucz do podpisywania</FieldLabel>
              <Input id="webhook-secret" placeholder="whsec_…" />
              <FieldDescription>
                Pozwala sprawdzić, czy żądania pochodzą od nas.
              </FieldDescription>
            </Field>
            <Field orientation="horizontal">
              <Switch id="webhook-retry" defaultChecked />
              <FieldLabel htmlFor="webhook-retry" className="font-normal">
                Ponawiaj nieudane dostarczenia
              </FieldLabel>
            </Field>
          </FieldGroup>
        </CollapsibleContent>
      </Collapsible>
      <Button className="w-fit">Zapisz webhook</Button>
    </div>
  )
}

Ruch

CollapsibleContent animuje zmierzoną wysokość (--collapsible-panel-height) z czasem trwania base i krzywą przejścia standard, w tym samym takcie płynnie pokazując lub ukrywając treść. Animację wyłączysz, nadpisując przejście w swojej kopii komponentu, a o ograniczony ruch nie musisz się martwić: przy prefers-reduced-motion panel otwiera się natychmiast.

Wytyczne

Kiedy używać

  • Do ukrycia jednego bloku drugorzędnej treści za czytelną etykietą: szczegółów, opcji zaawansowanych, reszty długiej listy.
  • Jako podstawa własnych wzorców rozwijania treści — drzew plików, sekcji paska bocznego, podsumowań z „pokaż więcej”.

Kiedy nie używać

  • Do kilku powiązanych sekcji, które mają działać jako zestaw — użyj komponentu Accordion.
  • Do treści, która ma unosić się nad stroną — użyj komponentu Popover.
  • Do ukrywania treści potrzebnej do wykonania bieżącego zadania.

Nazwij efekt

Wyzwalacz powinien mówić, co się pojawi — Ustawienia zaawansowane, Pokaż wszystkie zmiany, Jeszcze 3 pliki — a nie tylko Więcej. Połącz tekst z obracającą się strzałką; sama ikona nie mówi, co jest ukryte.

DobrzeEtykieta mówi, co się pokaże, jeszcze przed kliknięciem.
Źle„Więcej” nie mówi nic o tym, co jest ukryte ani ile tego jest.

Zostaw wyzwalacz na miejscu

Umieść wyzwalacz przed treścią, którą steruje, i nie przesuwaj go po otwarciu panelu. Użytkownik powinien móc zamknąć to, co właśnie otworzył, bez szukania kontrolki.

Dostępność

Wyzwalacz to natywny <button> z aria-expanded, a gdy panel jest otwarty — także z aria-controls wskazującym na panel.

KlawiszDziałanie
Tab
Przenosi fokus na wyzwalacz, a potem do treści otwartego panelu.
EnterSpacja
Otwiera lub zamyka panel.
  • Wyszukiwanie na stronie. Ustaw hiddenUntilFound na CollapsibleContent, a przeglądarka będzie mogła przeszukiwać zamknięty panel i otworzy go po znalezieniu dopasowania.
  • Fokus zostaje na miejscu. Otwarcie panelu nie przenosi fokusu; następne naciśnięcie Tab przeniesie go do środka.
  • Własne wyzwalacze. Gdy przez render renderujesz coś innego niż Button, upewnij się, że to nadal prawdziwy przycisk — albo przekaż nativeButton={false}, żeby Base UI dodał semantykę przycisku.

Dokumentacja API

Collapsible

Zarządza stanem otwarcia. Renderuje <div>. Przyjmuje wszystkie propsy Collapsible.Root z Base UI.

PropTypDomyślnie
defaultOpen

Czy panel jest otwarty przy pierwszym renderowaniu (tryb niekontrolowany).

booleanfalse
open

Czy panel jest otwarty (tryb kontrolowany). Używaj razem z onOpenChange.

booleanBrak wartości domyślnej
onOpenChange

Wywoływana, gdy panel się otwiera lub zamyka.

(open: boolean, details) => voidBrak wartości domyślnej
disabled

Blokuje otwieranie i zamykanie panelu.

booleanfalse
render

Podmienia element główny — np. na <li>, gdy Collapsible jest elementem listy.

ReactElement | (props, state) => ReactElementBrak wartości domyślnej

CollapsibleTrigger

Przycisk, który otwiera i zamyka panel. Gdy panel jest otwarty, dostaje data-panel-open — to przydatne przy obracaniu ikon. Przyjmuje wszystkie propsy Collapsible.Trigger z Base UI, w tym render i nativeButton.

CollapsibleContent

Panel. Domyślnie animuje swoją wysokość.

PropTypDomyślnie
hiddenUntilFound

Zostawia zamknięty panel w DOM z hidden="until-found", żeby wyszukiwanie na stronie mogło go odsłonić.

booleanfalse
keepMounted

Zostawia zamknięty panel w DOM. Ignorowany, gdy ustawiono hiddenUntilFound.

booleanfalse

Atrybuty data

AtrybutElementObecny, gdy
data-open / data-closedelement główny, panelPanel jest otwarty / zamknięty.
data-panel-openwyzwalaczPanel jest otwarty.
data-starting-style / data-ending-styleelement główny, panelPanel animuje się przy otwieraniu / zamykaniu.