05FAQ 아코디언 — 답변 높이 값을 내용 길이로 다시 구하는 목록
질문 하나를 연 상태가 되면 그 답변의 내용 높이 값을 다시 구해 그 값까지 펼쳐지고, 다른 질문을 열면 이전 질문은 높이 값이 0까지 줄며 접힙니다.
쓰임자주 묻는 질문 섹션, 상품 상세 하단 안내
scrollHeight--openclassList.remove

<main class="stage" id="stage">
<div class="faq is-demo" id="faq">
<div class="faq__item" style="--h:64px">
<button class="faq__q" type="button" aria-expanded="false" aria-controls="a1">
<span>배송은 얼마나 걸리나요?</span>
<span class="faq__caret" aria-hidden="true"></span>
</button>
<div class="faq__a" id="a1" role="region">
<p class="faq__a-inner">평균 1~2영업일 이내 출고되며, 도서 산간 지역은 최대 4일이 걸릴 수 있습니다.</p>
</div>
</div>
<div class="faq__item" style="--h:52px">
<button class="faq__q" type="button" aria-expanded="false" aria-controls="a2">
<span>환불 규정이 궁금해요</span>
<span class="faq__caret" aria-hidden="true"></span>
</button>
<div class="faq__a" id="a2" role="region">
<p class="faq__a-inner">수령 후 7일 이내 미개봉 상태면 전액 환불됩니다.</p>
</div>
</div>
<div class="faq__item" style="--h:58px">
<button class="faq__q" type="button" aria-expanded="false" aria-controls="a3">
<span>해외 배송도 되나요?</span>
<span class="faq__caret" aria-hidden="true"></span>
</button>
<div class="faq__a" id="a3" role="region">
<p class="faq__a-inner">일부 국가만 지원하며 결제 단계에서 배송 가능 여부가 표시됩니다.</p>
</div>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-base;
$easing: $ease-out;
$color: $subject-blue-deep;
@include stage($stage-ink);
@property --open {
syntax: "<number>";
inherits: true;
initial-value: 0;
}
.faq {
display: flex;
flex-direction: column;
gap: $sp-2;
width: 100%;
max-width: 340px;
}
.faq__item {
--open: 0;
border-radius: $r-card;
background: $stage-paper;
overflow: hidden;
}
.faq__q {
width: 100%;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
gap: $sp-2;
border: 0;
background: transparent;
padding: $sp-3 $sp-4;
color: $subject-ink;
font-family: inherit;
font-weight: 600;
font-size: 12px;
line-height: 1.4;
text-align: left;
cursor: pointer;
}
.faq__caret {
flex: 0 0 auto;
width: 7px;
height: 7px;
border-right: 2px solid $color;
border-bottom: 2px solid $color;
transform: rotate(calc(45deg + var(--open) * 180deg));
transition: transform $duration $easing;
}
.faq__a {
max-height: calc(var(--open) * var(--h));
overflow: hidden;
transition: max-height $duration $easing;
}
.faq__a-inner {
margin: 0;
padding: 0 $sp-4 $sp-3;
color: rgba($subject-ink, .78);
font-weight: 400;
font-size: 11px;
line-height: 1.5;
}
.is-demo .faq__item:nth-child(1) { animation: open-1 $dur-loop $easing infinite; }
.is-demo .faq__item:nth-child(2) { animation: open-2 $dur-loop $easing infinite; }
.is-demo .faq__item:nth-child(3) { animation: open-3 $dur-loop $easing infinite; }
@keyframes open-1 {
0%, 8% { --open: 0; }
22%, 45% { --open: 1; }
58%, 100% { --open: 0; }
}
@keyframes open-2 {
0%, 45% { --open: 0; }
58%, 78% { --open: 1; }
88%, 100% { --open: 0; }
}
@keyframes open-3 {
0%, 78% { --open: 0; }
88%, 96% { --open: 1; }
100% { --open: 0; }
}
@media (max-width: 340px) {
.faq__q { padding: $sp-2 $sp-3; font-size: 11px; }
.faq__a-inner { padding: 0 $sp-3 $sp-2; }
}
@media (prefers-reduced-motion: reduce) {
.is-demo .faq__item:nth-child(1),
.is-demo .faq__item:nth-child(2),
.is-demo .faq__item:nth-child(3) {
animation: none;
}
.faq__caret, .faq__a { transition: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
@property --open {
syntax: "<number>";
inherits: true;
initial-value: 0;
}
.faq {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
max-width: 340px;
}
.faq__item {
--open: 0;
border-radius: 16px;
background: #ffffff;
overflow: hidden;
}
.faq__q {
width: 100%;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
border: 0;
background: transparent;
padding: 12px 16px;
color: #17141a;
font-family: inherit;
font-weight: 600;
font-size: 12px;
line-height: 1.4;
text-align: left;
cursor: pointer;
}
.faq__caret {
flex: 0 0 auto;
width: 7px;
height: 7px;
border-right: 2px solid #0a4fe8;
border-bottom: 2px solid #0a4fe8;
transform: rotate(calc(45deg + var(--open) * 180deg));
transition: transform 300ms ease-out;
}
.faq__a {
max-height: calc(var(--open) * var(--h));
overflow: hidden;
transition: max-height 300ms ease-out;
}
.faq__a-inner {
margin: 0;
padding: 0 16px 12px;
color: rgba(23, 20, 26, 0.78);
font-weight: 400;
font-size: 11px;
line-height: 1.5;
}
.is-demo .faq__item:nth-child(1) {
animation: open-1 2s ease-out infinite;
}
.is-demo .faq__item:nth-child(2) {
animation: open-2 2s ease-out infinite;
}
.is-demo .faq__item:nth-child(3) {
animation: open-3 2s ease-out infinite;
}
@keyframes open-1 {
0%, 8% {
--open: 0;
}
22%, 45% {
--open: 1;
}
58%, 100% {
--open: 0;
}
}
@keyframes open-2 {
0%, 45% {
--open: 0;
}
58%, 78% {
--open: 1;
}
88%, 100% {
--open: 0;
}
}
@keyframes open-3 {
0%, 78% {
--open: 0;
}
88%, 96% {
--open: 1;
}
100% {
--open: 0;
}
}
@media (max-width: 340px) {
.faq__q {
padding: 8px 12px;
font-size: 11px;
}
.faq__a-inner {
padding: 0 12px 8px;
}
}
@media (prefers-reduced-motion: reduce) {
.is-demo .faq__item:nth-child(1),
.is-demo .faq__item:nth-child(2),
.is-demo .faq__item:nth-child(3) {
animation: none;
}
.faq__caret, .faq__a {
transition: none;
}
}// 질문을 열면 답변의 내용 높이 값을 scrollHeight 로 다시 구해 그 값까지 펼치고, 다른 질문을 열면 이전 답변은 0까지 줄며 접힌다.
// 계산 겹: 클릭 상태 -> scrollHeight 값 -> max-height 보간. 실사용 클릭 리스너 있음(데모 재생 자체는 고정 시퀀스).
(function () {
var faq = document.getElementById('faq');
if (!faq) return;
var items = faq.querySelectorAll('.faq__item');
function closeItem(item) {
var q = item.querySelector('.faq__q');
var a = item.querySelector('.faq__a');
if (!q || !a) return;
q.setAttribute('aria-expanded', 'false');
a.style.maxHeight = '0px';
}
function openItem(item) {
var q = item.querySelector('.faq__q');
var a = item.querySelector('.faq__a');
if (!q || !a) return;
q.setAttribute('aria-expanded', 'true');
a.style.maxHeight = a.scrollHeight + 'px';
}
items.forEach(function (item) {
var q = item.querySelector('.faq__q');
if (!q) return;
q.addEventListener('click', function () {
faq.classList.remove('is-demo');
var wasOpen = q.getAttribute('aria-expanded') === 'true';
items.forEach(closeItem);
if (!wasOpen) openItem(item);
});
});
})();import { useEffect, useRef } from "react";
import type { CSSProperties } from "react";
import styles from "./FaqAccordion.module.scss";
// 질문을 열면 답변의 내용 높이 값을 scrollHeight 로 다시 구해 그 값까지 펼치고, 다른 질문을 열면 이전 답변은 0까지 줄며 접힌다.
// 계산 겹: 클릭 상태 -> scrollHeight 값 -> max-height 보간. 실사용 클릭 리스너 있음(데모 재생 자체는 고정 시퀀스).
type Vars = CSSProperties & { "--h"?: string };
const ITEMS = [
{
id: "a1",
h: "64px",
q: "배송은 얼마나 걸리나요?",
a: "평균 1~2영업일 이내 출고되며, 도서 산간 지역은 최대 4일이 걸릴 수 있습니다.",
},
{
id: "a2",
h: "52px",
q: "환불 규정이 궁금해요",
a: "수령 후 7일 이내 미개봉 상태면 전액 환불됩니다.",
},
{
id: "a3",
h: "58px",
q: "해외 배송도 되나요?",
a: "일부 국가만 지원하며 결제 단계에서 배송 가능 여부가 표시됩니다.",
},
];
export default function FaqAccordion() {
const faqRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const faq = faqRef.current;
if (!faq) return;
const items = faq.querySelectorAll<HTMLElement>(`.${styles.faq__item}`);
function closeItem(item: HTMLElement) {
const q = item.querySelector<HTMLElement>(`.${styles.faq__q}`);
const a = item.querySelector<HTMLElement>(`.${styles.faq__a}`);
if (!q || !a) return;
q.setAttribute("aria-expanded", "false");
a.style.maxHeight = "0px";
}
function openItem(item: HTMLElement) {
const q = item.querySelector<HTMLElement>(`.${styles.faq__q}`);
const a = item.querySelector<HTMLElement>(`.${styles.faq__a}`);
if (!q || !a) return;
q.setAttribute("aria-expanded", "true");
a.style.maxHeight = a.scrollHeight + "px";
}
const handlers: Array<() => void> = [];
items.forEach((item) => {
const q = item.querySelector<HTMLElement>(`.${styles.faq__q}`);
if (!q) return;
const onClick = () => {
faq!.classList.remove(styles["is-demo"]);
const wasOpen = q.getAttribute("aria-expanded") === "true";
items.forEach(closeItem);
if (!wasOpen) openItem(item);
};
q.addEventListener("click", onClick);
handlers.push(() => q.removeEventListener("click", onClick));
});
return () => handlers.forEach((off) => off());
}, []);
return (
<main className={styles.stage}>
<div ref={faqRef} className={`${styles.faq} ${styles["is-demo"]}`}>
{ITEMS.map((item) => (
<div key={item.id} className={styles.faq__item} style={{ "--h": item.h } as Vars}>
<button className={styles.faq__q} type="button" aria-expanded="false" aria-controls={item.id}>
<span>{item.q}</span>
<span className={styles.faq__caret} aria-hidden="true" />
</button>
<div className={styles.faq__a} id={item.id} role="region">
<p className={styles["faq__a-inner"]}>{item.a}</p>
</div>
</div>
))}
</div>
</main>
);
}05 FAQ 아코디언 — 답변 높이 값을 내용 길이로 다시 구하는 목록 — 측정 누적면적 39.718% · 움직인 프레임 17/23GODRICH 원작 · zip 에 SCSS·React 포함 · 랜딩페이지 히어로섹션 디자인 9종 — 통계 카운터까지
이 데모가 속한 글
랜딩페이지 히어로섹션 디자인 9종 — 통계 카운터까지랜딩페이지 히어로섹션 아홉 개 중 하나는 카드가 보이는 비율 값이 기준을 넘을 때마다 순서 값에 간격을 곱해 나타나는 시점을 다시 구하고, 하나는 열린 질문의 답변 높이 값을 내용 길이로 다시 구해 펼치며, 또 하나 이 글의 데모 전부 보기


