아코디언

Accordion

제목을 누르면 본문이 펼쳐지고 닫히는 세로 목록. 대개 한 번에 하나만 열리게 제한합니다.

다른 이름: CollapseDisclosureExpanderExpansion panel
···
html
<div class="acc">
  <div class="item"><button class="head" aria-expanded="true" aria-controls="p1" id="h1">가격 정책</button>
    <div class="panel" id="p1" data-open role="region" aria-labelledby="h1"><div class="inner">월 9,900원부터 시작하고 언제든 해지할 수 있어요.</div></div></div>
  <div class="item"><button class="head" aria-expanded="false" aria-controls="p2" id="h2">환불 규정</button>
    <div class="panel" id="p2" role="region" aria-labelledby="h2"><div class="inner">구매 후 7일 이내라면 100% 환불됩니다.</div></div></div>
  <div class="item"><button class="head" aria-expanded="false" aria-controls="p3" id="h3">지원 채널</button>
    <div class="panel" id="p3" role="region" aria-labelledby="h3"><div class="inner">이메일과 채팅으로 상시 지원합니다.</div></div></div>
</div>
css
.acc{width:min(380px,92%);display:grid;gap:8px}
.item{border:1px solid var(--line);border-radius:10px;overflow:hidden;background:var(--surface)}
.head{width:100%;text-align:left;padding:10px 14px;border:0;background:none;color:var(--fg);font-size:13px;font-weight:600;cursor:pointer;display:flex;justify-content:space-between;align-items:center}
.head::after{content:"+";color:var(--accent);font-size:16px;transition:transform .25s}
.head[aria-expanded=true]::after{transform:rotate(45deg)}
.panel{display:grid;grid-template-rows:0fr;transition:grid-template-rows .28s ease}
.panel[data-open]{grid-template-rows:1fr}
.inner{overflow:hidden;min-height:0;padding:0 14px;color:var(--muted);font-size:12px;line-height:1.5}
.panel[data-open] .inner{padding-bottom:12px}
js
const heads = [...document.querySelectorAll('.head')];
const panels = [...document.querySelectorAll('.panel')];
function show(i) {
  heads.forEach((h, idx) => h.setAttribute('aria-expanded', String(idx === i)));
  panels.forEach((p, idx) => idx === i ? p.setAttribute('data-open', '') : p.removeAttribute('data-open'));
}
let auto = true, idx = 0;
heads.forEach((h, i) => h.addEventListener('click', () => {
  auto = false;
  const willOpen = h.getAttribute('aria-expanded') !== 'true';
  show(willOpen ? i : -1);
}));
setInterval(() => { if (!auto) return; idx = (idx + 1) % heads.length; show(idx); }, 2400);

버튼(헤더) 하나와 그 아래 패널이 한 쌍입니다. 헤더는 aria-expanded로 열림 상태를, aria-controls로 자신이 여는 패널의 id를 가리킵니다. 높이 애니메이션은 grid-template-rows를 0fr↔1fr로 바꾸는 CSS 트릭으로 만들 수 있어 JS로 height를 재는 것보다 가볍습니다.

트리뷰와 다른 점은 계층 깊이입니다 — 아코디언은 보통 1단계 섹션을 여닫지만 트리뷰는 폴더 안의 폴더처럼 몇 단계든 중첩됩니다. 탭과 다른 점은 배치입니다 — 탭은 가로로 나열돼 콘텐츠 영역 하나만 남기고 바꿔치기하지만, 아코디언은 세로로 쌓이고 여러 섹션이 동시에 펼쳐질 수도 있습니다.

헤더는 반드시 진짜 button이어야 합니다(div+onclick 금지) — 그래야 Tab으로 순서대로 오고 Enter/Space로 토글됩니다. 패널에는 role="region"과 aria-labelledby로 자신을 연 헤더를 알려주면 스크린리더 사용자가 목차처럼 훑을 수 있습니다.

언제 쓰나

FAQ, 필터 그룹, 모바일 화면처럼 세로 공간이 귀한 곳에서 섹션을 정리할 때 씁니다. 한 화면에 3~7개 정도가 적당합니다.