JavaScript-сіз тегіс аккордеон
Аккордеон құру үшін бәрі React күйіне және биіктікті өлшейтін хукке жүгінеді. Ал браузерде мұндай нативтік элемент жылдар бойы бар — әрі 2024 жылдан бастап ол таза CSS арқылы ашылуы мен жабылуын анимациялайды. Мұнда толық техника: details[name], ::details-content, interpolate-size және жабылуды да анимациялайтын жалғыз қасиет.
Аккордеон — вебте ең жиі қайта жазылатын виджеттердің бірі. Әдеттегі рецепт: не ашық екенін сақтайтын React күйінің бөлігі, оны ауыстыратын onClick және ашылуды анимациялайтын біраз CSS (немесе биіктікті өлшейтін хук). Ол жұмыс істейді, бірақ статикалық контенттің бір бөлігін JavaScript жеткізіп, гидратациялайтын клиенттік компонентке айналдырады — тек ашу мен жабу үшін.
Браузерде нативтік аккордеон элементі жылдар бойы бар: <details>. Оның жалғыз тарихи әлсіздігі — оны анимациялау мүмкін болмауы — 2024 жылдан бастап жойылды. Бұл мақалада айналатын шеврені бар, толық анимацияланған, эксклюзивті аккордеон нөлдік JavaScript-пен құрылады: күйсіз, хуксіз, гидратациясыз.
Бәрі жеткізетін аккордеон
Міне, ең кең тараған түрдегі паттерн — ашық элементке арналған React күйі, оны ауыстыратын клик өңдеуші және ешнәрсені өлшемей биіктікті анимациялайтын grid-template-rows: 0fr → 1fr амалы:
"use client"; // ← the whole component is now a client bundle
import { useState } from "react";
export function Accordion({ items }: { items: Item[] }) {
const [openId, setOpenId] = useState<string | null>(null);
return (
<div>
{items.map((item) => {
const isOpen = openId === item.id;
return (
<div key={item.id}>
<button onClick={() => setOpenId(isOpen ? null : item.id)}>
{item.title}
</button>
{/* grid 0fr → 1fr is the CSS trick that animates the height */}
<div className="body" data-open={isOpen}>
<div>{item.content}</div>
</div>
</div>
);
})}
</div>
);
}.body {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 300ms;
}
.body[data-open="true"] { grid-template-rows: 1fr; }
.body > div { overflow: hidden; }Бұл жаман емес, әрі grid амалы шынымен де тапқыр. Бірақ оның не тұратынына назар аударыңыз: жоғарыдағы "use client". Бұл компонент енді ол шыққан әр бетте жүктеледі, талданады және гидратацияланады — және мұның бәрін жалғыз буль мәнін басқару үшін жасайды. React мағынасында ешқашан интерактивті болу қажет болмаған презентациялық FAQ үшін бұл таза үстеме шығын.
Сіз ұмытқан нативтік элемент
<details> мен <summary> — кіріктірілген ашу виджеті. Браузер ашу/жабу күйін, клик пен пернетақта әрекеттесуін және қолжетімділікті — тегін өңдейді:
<details>
<summary>What does it cost?</summary>
<p>Nothing — it's a native browser element.</p>
</details>Бұл — JavaScript-сіз және CSS-сіз жұмыс істейтін аккордеон. Summary-ге басыңыз (немесе Enter басыңыз) — ол ауысады. Жылдар бойы кедергі болғаны — ол лезде ауысатын: ашылуды анимациялаудың ешқандай жолы болмады, дәл сондықтан командалар оның орнына JavaScript нұсқасына жүгінетін.
Ол неге анимациялана алмады — соңғы уақытқа дейін
Себебі нәзік. <details> жабық кезде браузер оның контентін тек көзге көрінбейтін етіп жасырмайды — контентті рендерлеуден мүлдем шығарып тастайды (content-visibility тәріздес жасыру). Сіз "рендерленбеген" күйге не одан ауыса алмайсыз, әрі ұзақ уақыт бойы height мәнін auto-ға не одан да ауыстыра алмайтынсыз. Сондықтан анимациялайтын ештеңе болмады.
Адамдар JavaScript нұсқасы қолданатын дәл сол grid амалын [open] атрибутына көшіріп көрді:
/* Looks like it should work — but only animates OPENING. */
details > .body {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 300ms;
}
details[open] > .body { grid-template-rows: 1fr; }
/* On close, the browser hides the content the instant [open] is gone,
so the collapse never animates — it just snaps shut. */Ол дұрыс көрінеді, тіпті ашылуды да анимациялайды. Бірақ жабылуда сәтсіздікке ұшырайды: [open] атрибуты жойылған сәтте браузер контентті дереу жасырады, сондықтан жиырылу анимациялануға мүмкіндік алмайды — ол жай ғана лезде жабылады. Жарты шешім бәрін JavaScript-те ұстап тұрды.
Шешім: ::details-content + interpolate-size
2024 жылы шыққан үш CSS мүмкіндігі оны бірге шешеді. Кілт — жаңа псевдоэлемент ::details-content, ол <details>-тың дәл жиырылатын бөлігін нысанаға алады — сондықтан оны тікелей стильдеп, анимациялауға болады:
:root {
/* lets height/block-size transition to and from the 'auto' keyword */
interpolate-size: allow-keywords;
}
details::details-content {
block-size: 0;
overflow: clip;
transition:
block-size 300ms ease,
/* keeps the content rendered while it collapses, instead of vanishing */
content-visibility 300ms allow-discrete;
}
details[open]::details-content {
block-size: auto;
}Бір бүтін ретінде оқығанда, бұл бірге әрекеттесетін үш нәрсе:
::details-contentсізге бұрын мүлдем таңдай алмаған жасырын контент аймағына тұтқа береді.interpolate-size: allow-keywordsblock-sizeмәнін0мен ішкіautoбиіктігі арасында анимациялауға рұқсат етеді — CSS тарихи түрде істеуден бас тартқан нәрсе.transition-behavior: allow-discrete(content-visibility-ді transition-да тізу арқылы) контентті жиырылу бойы рендерленген күйде ұстайды, сондықтан жабылу лезде емес, анимацияланады.
Бірге олар екі бағытта да — ашуды да, жабуды да — анимациялайды, биіктік контенттің табиғи өлшеміне және одан тегіс жеңілдей отырып. Өлшеусіз, хуксіз, күйсіз.
Эксклюзивті аккордеон, бір атрибутпен
Аккордеондардың көбіне "бір уақытта тек біреуі ашық" қажет. JavaScript нұсқасында openId күйі дәл осы үшін керек. Нативтік түрде бұл — жалғыз атрибут: топтағы әр <details>-қа бірдей name беріңіз, сонда браузер эксклюзивтілікті қамтамасыз етеді — біреуін ашу қалғандарын жабады:
<!-- Same name = one exclusive group. Opening one closes the others. -->
<details name="faq"><summary>First</summary><p>…</p></details>
<details name="faq"><summary>Second</summary><p>…</p></details>
<details name="faq"><summary>Third</summary><p>…</p></details>Білуге тұрарлық бір ескерту: name бүкіл бет бойынша эксклюзивті топ анықтайды, сондықтан екі тәуелсіз аккордеон рендерлесеңіз, әрқайсысына өзінің бірегей name-ін беріңіз — әйтпесе біреуіндегі элементті ашу екіншісіндегі элементті жабады.
Шеврен, JavaScript-сіз
Адамдар JS-ке жүгінетін соңғы бөлік — бөлім ашылғанда аударылатын кішкентай бағдарша. Бұл — жай ғана [open] атрибутынан кілтенген transform — браузер сол атрибутты орнатады, сондықтан айналу таза CSS. Осы жерде әдепкі ашу үшбұрышын да жасырыңыз:
summary {
list-style: none; /* remove the default triangle marker */
cursor: pointer;
}
summary::-webkit-details-marker { display: none; } /* Safari */
summary .chevron {
transition: transform 200ms ease;
}
details[open] summary .chevron {
transform: rotate(180deg); /* flip on open — pure CSS, no JS */
}Барлығын біріктіру
Міне, толық нұсқасы — айналатын шеврені бар және сұрақ пен жауап арасында аралығы бар эксклюзивті, анимацияланған аккордеон элементі:
<details name="faq" class="item">
<summary>
<h3>How does it work?</h3>
<svg class="chevron" aria-hidden="true"><!-- ↓ --></svg>
</summary>
<div class="answer">
<p>Native open/close, exclusive grouping, smooth animation — zero JS.</p>
</div>
</details>:root { interpolate-size: allow-keywords; }
.item::details-content {
block-size: 0;
overflow: clip;
transition:
block-size 300ms ease,
content-visibility 300ms allow-discrete;
}
.item[open]::details-content { block-size: auto; }
.item[open] summary .chevron { transform: rotate(180deg); }
.answer { padding-top: 12px; } /* gap between question and answer */Бұл — толық мүмкіндік. Ашу/жабу, пернетақта қолдауы, қолжетімділік, эксклюзивтілік, тегіс екіжақты анимация және аударылатын шеврен — және ол бірде-бір байт JavaScript жеткізбейді. Ол серверде статикалық HTML ретінде рендерленеді және жұмыс істеу үшін клиентте ештеңені қажет етпейді.
Браузер қолдауы
::details-content, interpolate-size және allow-discrete — жаңа мүмкіндіктер (Chromium 129–131, Safari 18.x, Firefox артынан). Бұл қауіпті естіледі, бірақ деградация — ең жақсы түрі — сізге @supports мүлдем қажет емес:
/* No @supports needed: browsers that don't know ::details-content simply
ignore these rules. The <details> still opens and closes — just instantly.
The animation is a progressive enhancement, never a requirement. */Бұл ережелерді түсінбейтін браузерде <details> дұрыс ашылып, жабылады; ол мұны жай ғана анимациясыз, лезде жасайды. Анимация — бұрыннан барлық жерде жұмыс істейтін элементтің үстіне қабатталған прогрессивті жақсарту — сондықтан жазатын резервтік нұсқа да, бұзылатын ештеңе де жоқ.
Мінез-құлық — ашу, жабу, пернетақта, қолжетімділік, эксклюзивтілік — нативтік және барлық жерде жұмыс істейді. Анимация — оның үстіндегі таза CSS. Егер браузер анимация үшін тым ескі болса, аккордеон әлі де жұмыс істейді; ол тек жеңілдемейді.
CSS пен JavaScript, шынын айтқанда
| Мәселе | JS аккордеон (күй + хук) | Нативтік <code><details></code> + CSS |
|---|---|---|
| Ашу / жабу | React күйі + onClick | Браузерге кіріктірілген |
| Жеткізілген JavaScript | Иә (клиенттік компонент, гидратацияланады) | Жоқ |
| Тегіс анимация | Иә (grid / өлшенген биіктік) | Иә (::details-content) |
| Тек-біреуі-ашық | Қолмен басқарылатын күй (openId) | name атрибуты |
| Пернетақта + a11y | Оны өзіңіз жалғайсыз (немесе кітапхана) | Нативтік |
| Гидратацияға дейін / JS өшірулі кезде жұмыс істейді | Жоқ | Иә |
| Шевреннің аударылуы | JS ауыстыратын класс | [open]-дегі CSS |
Панель контенті шынымен динамикалық болғанда — сұраныс бойынша жүктелгенде, қолданба күйімен басқарылғанда немесе ашу сіз JS-те бақылайтын жанама әсерлерді іске қосуы керек болғанда — JavaScript әлі де жеңеді. Бірақ өте кең тараған жағдайда — презентациялық FAQ немесе ашу тізімінде — нативтік элемент JS нұсқасына мүмкіндік бойынша тең келеді, бумасыз.
Түйіндер
- Бәрі жазатын JavaScript аккордеоны негізінен нативтік
<details>анимациялана алмағандықтан бар болды — енді жойылған шектеу. ::details-content+interpolate-size: allow-keywordsбиіктікті0менautoарасында анимациялайды.- Жабылуды (тек ашылуды ғана емес) анимациялайтын амал —
content-visibility-дегіtransition-behavior: allow-discrete. <details name="…">сізге нөлдік JavaScript-пен эксклюзивті "бір уақытта біреуі ашық" тобын береді — әр аккордеонға бірегей атау қолданыңыз.- Шевреннің аударылуы —
[open]атрибутынан кілтенген CSStransform. - Ол
@supports-сыз ұқыпты деградацияланады: ескі браузерлер лезде ашып/жабады, ал анимация — таза жақсарту.
Бұл бөліктердің ешқайсысы экзотикалық емес — <details> ондаған жылдардан бері бар, ал анимация мүмкіндіктері — бұрыннан белгілі, жақсы спецификацияланған CSS. Өзгеріс — жылтыратылған аккордеон алу үшін енді React күйіне жүгінудің қажеті жоқ екенін ұғынуда. Клиенттік компонентті жойыңыз, статикалық HTML жеткізіңіз және браузерге бұрыннан үнсіз істей алатын нәрсесін істеуге мұрсат беріңіз.