클릭하면 펼쳐지고 다시 클릭하면 접히는 아코디언. FAQ, 메뉴, 콘텐츠 접기에 필수적인 UI 패턴을 CSS transition과 JS로 부드럽게 만들어봐요.
클릭 한 번으로 콘텐츠를 펼치고 접는 UI 컴포넌트예요. 좁은 공간에 많은 정보를 담을 수 있어요.
핵심 트릭: max-height: 0 → max-height: 500px 전환이에요. height: auto에는 transition이 안 되지만, max-height에는 돼요. 이 한 가지 CSS 트릭으로 부드러운 펼침/접힘을 구현할 수 있어요.
자주 묻는 질문을 아코디언으로 만들면 질문 목록을 한눈에 볼 수 있고, 원하는 답변만 펼쳐 볼 수 있어요. 고객 지원 페이지의 필수 패턴이에요.
모바일에서 상단 카테고리를 아코디언으로 만들면 서브메뉴를 터치로 펼칠 수 있어요. 쇼핑몰 카테고리, 블로그 태그 목록에 자주 써요.
고급 설정, 필터 옵션 등을 아코디언으로 접어두면 기본 화면이 깔끔해져요. 사용자가 필요할 때만 펼쳐서 상세 설정을 볼 수 있어요.
상품 설명, 약관, 긴 텍스트를 "더 보기" 버튼으로 접어두는 패턴도 아코디언의 변형이에요. 스크롤을 줄여 UX를 향상시켜요.
각 항목을 클릭해서 아코디언이 펼쳐지는 것을 확인해보세요. 기본형은 하나 열면 다른 것이 자동으로 닫혀요.
① 기본형 — FAQ 아코디언 (하나만 열림)
② 보더형 — 테두리 강조 스타일
:root { --accent: #4f46e5; }처럼 전역 변수를 선언하고, color: var(--accent)로 사용해요. 색상을 한 곳에서 바꾸면 전체 사이트에 반영돼요. 다크모드 구현에도 핵심적인 패턴이에요.
const res = await fetch(url);로 API 데이터를 비동기로 가져올 수 있어요. 날씨 앱, 환율 계산기 같은 실시간 데이터를 받아오는 기능에 꼭 필요해요. async/await 패턴과 함께 사용해요.
@media (max-width: 600px) { ... }로 화면 크기별 스타일을 지정해요. Flexbox와 Grid의 auto-fill, minmax()를 활용하면 미디어쿼리 없이도 반응형이 되는 경우가 많아요.
③ 다중 오픈형 — 여러 개 동시에 열기동시 열림
<title>과 <meta name="description">은 검색 결과 화면에 표시돼요. title은 60자, description은 160자 이내로 핵심 키워드를 포함해 작성하세요. <link rel="canonical">로 중복 URL 문제도 잡아요.
[data-theme="dark"] 속성 하나로 전체 색상을 바꿀 수 있어요. localStorage에 설정을 저장해 새로고침해도 유지되게 해요. 바이브툴킷 다크모드 페이지에서 전체 코드를 볼 수 있어요.
await navigator.clipboard.writeText(text) 세 줄이면 구현 완료예요. 복사 완료 시 버튼 색상을 초록으로 바꿔 피드백을 주고, 2초 후 원래대로 돌아오게 하면 좋은 UX가 완성돼요.
복사해서 바로 쓸 수 있는 완전한 아코디언 코드예요.
<div class="accordion" id="myAccordion"> <div class="acc-item"> <button class="acc-btn" onclick="toggleSingle(this,'myAccordion')"> 첫 번째 질문이에요 <span class="acc-arrow">▼</span> </button> <div class="acc-body"> <div class="acc-body-inner"> 첫 번째 답변 내용을 여기에 써요. </div> </div> </div> <div class="acc-item"> <button class="acc-btn" onclick="toggleSingle(this,'myAccordion')"> 두 번째 질문이에요 <span class="acc-arrow">▼</span> </button> <div class="acc-body"> <div class="acc-body-inner"> 두 번째 답변 내용을 여기에 써요. </div> </div> </div> </div>
/* 핵심: max-height 전환으로 슬라이드 효과 구현 */ .acc-body { max-height: 0; overflow: hidden; transition: max-height .35s ease; } .acc-item.open .acc-body { max-height: 500px; /* 콘텐츠보다 크면 됨 */ } /* 화살표 회전 애니메이션 */ .acc-arrow { display: inline-flex; transition: transform .3s ease; } .acc-item.open .acc-arrow { transform: rotate(180deg); } /* 아코디언 버튼 기본 스타일 */ .acc-btn { width: 100%; display: flex; align-items: center; justify-content: space-between; padding: 16px 20px; background: none; border: none; font-family: inherit; font-size: .93rem; font-weight: 600; color: #111827; cursor: pointer; text-align: left; gap: 12px; transition: background .15s, color .2s; } .acc-btn:hover { background: #f9fafb; } .acc-item.open .acc-btn { color: #4f46e5; }
// ① 단일 오픈 (하나 열면 다른 것 자동으로 닫힘) function toggleSingle(btn, containerId) { const container = document.getElementById(containerId); const item = btn.closest('.acc-item'); const isOpen = item.classList.contains('open'); // 모든 항목 닫기 container.querySelectorAll('.acc-item').forEach(function(el) { el.classList.remove('open'); }); // 클릭한 항목이 닫혀있었으면 열기 if (!isOpen) { item.classList.add('open'); } } // ② 다중 오픈 (각자 독립적으로 열고 닫힘) function toggleMulti(btn) { btn.closest('.acc-item').classList.toggle('open'); } // ③ 전체 닫기 버튼 (선택사항) function closeAll(containerId) { document.getElementById(containerId) .querySelectorAll('.acc-item') .forEach(function(el) { el.classList.remove('open'); }); }
기본 아코디언을 더 정교하게 만드는 팁이에요.
max-height 값 설정 주의: max-height: 500px로 설정했는데 콘텐츠가 500px를 넘으면 잘려요. 콘텐츠 최대 높이보다 여유 있게 설정하거나, 동적으로 scrollHeight를 읽어서 설정하는 방법을 쓰면 돼요.
콘텐츠 높이가 가변적일 때 scrollHeight를 동적으로 읽어 설정하면 어떤 내용도 안전하게 펼쳐져요.
function toggleAccurate(btn) { const item = btn.closest('.acc-item'); const body = item.querySelector('.acc-body'); if (item.classList.contains('open')) { // 닫기: 현재 높이 → 0 body.style.maxHeight = body.scrollHeight + 'px'; // 다음 프레임에서 0으로 설정해야 transition이 동작 requestAnimationFrame(() => { body.style.maxHeight = '0'; }); item.classList.remove('open'); } else { // 열기: 0 → scrollHeight item.classList.add('open'); body.style.maxHeight = body.scrollHeight + 'px'; // transition 끝나면 auto로 (내용 늘어나도 대응) body.addEventListener('transitionend', () => { body.style.maxHeight = 'none'; }, { once: true }); } }
transform: rotate(180deg)과 transition으로 ▼가 부드럽게 ▲로 회전해요. 단순한 텍스트 ▼ 대신 SVG 아이콘을 쓰면 더 세련돼요.
아코디언 안에 아코디언을 넣을 수 있어요. 단, max-height가 중첩되면 계산이 복잡해져요. 중첩이 깊어지면 scrollHeight 방식으로 전환하는 것을 추천해요.
버튼에 aria-expanded="false/true", 패널에 aria-hidden="true/false"를 추가해요. 스크린 리더가 "닫혀있음/열려있음"을 읽어줘서 장애인 사용자도 쓸 수 있어요.
아코디언 UI를 AI에게 요청할 때 이 프롬프트를 참고해보세요.
아래 프롬프트를 복사해서 AI에 붙여넣어 보세요.
내 웹사이트 하단에 FAQ 아코디언을 만들어줘. 질문/답변 목록: 1. 배송은 얼마나 걸리나요? / 평균 2~3 영업일이 소요돼요. 2. 교환/환불은 어떻게 하나요? / 수령 후 7일 이내 문의주세요. 3. 재고가 없으면 어떻게 되나요? / 입고 알림을 신청할 수 있어요. 4. 해외 배송도 되나요? / 현재 국내 배송만 운영 중이에요. 요구사항: - 클릭하면 max-height 트랜지션으로 부드럽게 펼쳐짐 - 하나 열면 다른 것은 자동으로 닫힘 (단일 오픈) - 화살표(▼)가 열릴 때 180도 회전 - 열린 항목 헤더 색상: 인디고 (#4f46e5) - 모바일에서도 자연스럽게 동작 - 순수 HTML + CSS + Vanilla JS (라이브러리 없이)
아코디언 구현 시 자주 맞닥뜨리는 질문들이에요.
auto는 고정 수치가 아닌 브라우저가 계산하는 값이라 보간이 불가능해요. 그래서 max-height: 0 → 500px처럼 구체적인 수치 사이에서는 transition이 잘 동작해요. CSS Grid의 grid-template-rows: 0fr → 1fr 트릭도 비슷한 문제를 해결하는 현대적 방법이에요.
<details>와 <summary> 태그를 사용하면 JavaScript 없이 기본 아코디언을 만들 수 있어요. <details open>으로 기본 열림 설정도 돼요. 단, 브라우저 기본 스타일이 제한적이고 애니메이션이 없어요. CSS animation을 details[open]에 적용하면 어느 정도 개선할 수 있어요.
useState로 열린 항목의 인덱스를 관리해요. 단일 오픈은 const [openIdx, setOpenIdx] = useState(null), 다중 오픈은 const [openSet, setOpenSet] = useState(new Set())를 사용해요. Radix UI의 Accordion 컴포넌트를 쓰면 접근성까지 포함된 완성된 컴포넌트를 바로 사용할 수 있어요.