liquiddesign

Wzorzec 11

Akordeon bez mierzenia

Przez lata animacja akordeonu wymagała mierzenia treści w JavaScript, bo height auto nie daje się animować. Siatka to umie: wiersz przechodzi płynnie od 0fr do 1fr, a treść po prostu wypełnia dostępny ułamek.

Najczęstsze pytania klientów

Niewidzialna kopia w tej samej komórce siatki rezerwuje wysokość najwyższego stanu. Akordeon animuje się swobodnie, reszta strony zostaje na miejscu.

Po warsztacie zakresu dzielimy pracę na etapy z własnymi budżetami: tożsamość, system, wdrożenie. Każdy etap kończy się działającym artefaktem, nie prezentacją.

Reguły

  • Rozwijanie animuje grid-template-rows, nie height ani max-height z magiczną wartością.
  • Wewnętrzny kontener ma overflow hidden i min-height 0, żeby ułamek 0fr naprawdę znikał.
  • Przycisk nagłówka niesie aria-expanded i wskazuje panel przez aria-controls.
  • prefers-reduced-motion wyłącza animację, stan zmienia się natychmiast.

Wzorzec w kodzie

Niewidzialna kopia rezerwuje wysokość, akordeon animuje się w środku
<div className="grid">
  <dl aria-hidden className="invisible col-start-1 row-start-1">
    {items.map((item) => (
      <dt key={item.id}>
        <span className={headerClassName}>{item.question}</span>
      </dt>
    ))}
    <dd className="grid">
      {items.map((item) => (
        <p key={item.id} className={`col-start-1 row-start-1 ${answerClassName}`}>
          {item.answer}
        </p>
      ))}
    </dd>
  </dl>

  <dl className="col-start-1 row-start-1 self-start">
    {items.map((item) => (
      <>
        <dt></dt>
        <dd
          className="grid transition-[grid-template-rows] duration-300"
          style={{ gridTemplateRows: isOpen ? "1fr" : "0fr" }}
        >
          <div className="min-h-0 overflow-hidden">
            <p className={answerClassName}>{item.answer}</p>
          </div>
        </dd>
      </>
    ))}
  </dl>
</div>
Pytanie to dt z przyciskiem i aria-expanded
<dt>
  <h3>
    <button
      aria-expanded={openId === id}
      aria-controls={panelId}
      onClick={() => setOpenId(openId === id ? null : id)}
    >
      {question}
    </button>
  </h3>
</dt>