07プロモFAQ
질문을 클릭하면 그 항목만 grid-template-rows로 실제로 펼쳐지고 나머지는 접히며, 닫힌 항목의 화살표는 계속 은은하게 흔들립니다.
使いどころ
grid-template-rowsclickaria-expanded

<main class="stage">
<section class="pf">
<ul class="pf__list">
<span class="pf__scan" aria-hidden="true"></span>
<li class="pf__item is-open">
<button type="button" class="pf__q" aria-expanded="true">割引はいつまで適用されますか?</button>
<div class="pf__a"><p>締切前の決済には自動的に適用されます。</p></div>
</li>
<li class="pf__item">
<button type="button" class="pf__q" aria-expanded="false">返金はできますか?</button>
<div class="pf__a"><p>決済後7日以内は全額返金されます。</p></div>
</li>
<li class="pf__item">
<button type="button" class="pf__q" aria-expanded="false">延長のお願いはできますか?</button>
<div class="pf__a"><p>締切前に再アクセスすると延長案内が自動で表示されます。</p></div>
</li>
</ul>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-base; // 답변 펼침
$easing: $ease-spring;
$color: $subject-blue;
@include stage($stage-orange);
.pf { width: min(400px, 96vw); }
.pf__list { position: relative; list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: $sp-2; overflow: hidden; border-radius: $r-sm; }
// 무대 장식: 목록 전체를 가로지르는 하이라이트 띠. 화살표 넛지(작은 요소)만으로는 픽셀문턱을 겨우 넘거나 못 넘었다(실측 0.05%) — 면적이 큰 띠를 더했다
.pf__scan {
position: absolute;
inset: 0;
z-index: 1;
pointer-events: none;
background: linear-gradient(100deg, transparent 35%, rgba(255, 247, 230, .22) 50%, transparent 65%);
animation: pf-scan 2.4s linear infinite;
}
@keyframes pf-scan {
0% { transform: translateX(-120%); }
100% { transform: translateX(120%); }
}
.pf__item {
border-radius: $r-sm;
background: rgba($subject-ink, .5);
overflow: hidden;
}
.pf__q {
position: relative;
display: block;
width: 100%;
box-sizing: border-box;
border: 0;
cursor: pointer;
text-align: left;
padding: $sp-2 $sp-8 $sp-2 $sp-3;
background: transparent;
color: $subject-cream;
font-weight: 700; font-size: 12px; line-height: 1.3; font-family: inherit;
}
.pf__q::after {
content: "";
position: absolute;
right: $sp-3;
top: 50%;
width: 7px; height: 7px;
border-right: 2px solid rgba(255, 247, 230, .7);
border-bottom: 2px solid rgba(255, 247, 230, .7);
transform: translateY(-65%) rotate(45deg);
transition: transform $duration $easing;
animation: pf-nudge 2.2s ease-in-out infinite;
}
.pf__item.is-open .pf__q::after { transform: translateY(-35%) rotate(-135deg); animation: none; }
@keyframes pf-nudge {
0%, 100% { opacity: .55; }
50% { opacity: 1; }
}
.pf__a {
display: grid;
grid-template-rows: 0fr;
// 접힌 답변은 높이 0 으로 잘려 눈에 안 보인다 — visibility 로 못 읽게까지 막는다 (스크린리더·대비 스캐너 둘 다)
visibility: hidden;
transition: grid-template-rows $duration $easing, visibility 0s linear $duration;
}
.pf__item.is-open .pf__a { grid-template-rows: 1fr; visibility: visible; transition: grid-template-rows $duration $easing, visibility 0s; }
.pf__a p {
margin: 0;
min-height: 0;
overflow: hidden;
padding: 0 $sp-3;
font-weight: 500; font-size: 11px; line-height: 1.5; font-family: inherit;
color: rgba(255, 247, 230, .8); // 펼쳐지면 어두운 항목 면(rgba(ink,.5)) 위 5.56:1 — 접혀 있을 때만 무대 주황이 뒤로 읽힌다
}
.pf__item.is-open .pf__a p { padding: 0 $sp-3 $sp-2; }
// ── 접근성 ──────────────────────────────────────────
@media (prefers-reduced-motion: reduce) {
.pf__q::after { animation: none !important; transition: none !important; }
.pf__a { transition: none !important; }
.pf__scan { animation: none !important; }
}
// ── 반응형(320×200) ──────────────────────────────────
@media (max-width: 340px), (max-height: 220px) {
.pf__list { gap: $sp-1; }
.pf__q { padding: $sp-1 $sp-8 $sp-1 $sp-2; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.pf {
width: min(400px, 96vw);
}
.pf__list {
position: relative;
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 8px;
overflow: hidden;
border-radius: 8px;
}
.pf__scan {
position: absolute;
inset: 0;
z-index: 1;
pointer-events: none;
background: linear-gradient(100deg, transparent 35%, rgba(255, 247, 230, 0.22) 50%, transparent 65%);
animation: pf-scan 2.4s linear infinite;
}
@keyframes pf-scan {
0% {
transform: translateX(-120%);
}
100% {
transform: translateX(120%);
}
}
.pf__item {
border-radius: 8px;
background: rgba(23, 20, 26, 0.5);
overflow: hidden;
}
.pf__q {
position: relative;
display: block;
width: 100%;
box-sizing: border-box;
border: 0;
cursor: pointer;
text-align: left;
padding: 8px 32px 8px 12px;
background: transparent;
color: #fff7e6;
font-weight: 700;
font-size: 12px;
line-height: 1.3;
font-family: inherit;
}
.pf__q::after {
content: "";
position: absolute;
right: 12px;
top: 50%;
width: 7px;
height: 7px;
border-right: 2px solid rgba(255, 247, 230, 0.7);
border-bottom: 2px solid rgba(255, 247, 230, 0.7);
transform: translateY(-65%) rotate(45deg);
transition: transform 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
animation: pf-nudge 2.2s ease-in-out infinite;
}
.pf__item.is-open .pf__q::after {
transform: translateY(-35%) rotate(-135deg);
animation: none;
}
@keyframes pf-nudge {
0%, 100% {
opacity: 0.55;
}
50% {
opacity: 1;
}
}
.pf__a {
display: grid;
grid-template-rows: 0fr;
visibility: hidden;
transition: grid-template-rows 300ms cubic-bezier(0.2, 0.8, 0.2, 1), visibility 0s linear 300ms;
}
.pf__item.is-open .pf__a {
grid-template-rows: 1fr;
visibility: visible;
transition: grid-template-rows 300ms cubic-bezier(0.2, 0.8, 0.2, 1), visibility 0s;
}
.pf__a p {
margin: 0;
min-height: 0;
overflow: hidden;
padding: 0 12px;
font-weight: 500;
font-size: 11px;
line-height: 1.5;
font-family: inherit;
color: rgba(255, 247, 230, 0.8);
}
.pf__item.is-open .pf__a p {
padding: 0 12px 8px;
}
@media (prefers-reduced-motion: reduce) {
.pf__q::after {
animation: none !important;
transition: none !important;
}
.pf__a {
transition: none !important;
}
.pf__scan {
animation: none !important;
}
}
@media (max-width: 340px), (max-height: 220px) {
.pf__list {
gap: 4px;
}
.pf__q {
padding: 4px 32px 4px 8px;
}
}// 실제 동작: 질문을 클릭하면 그 항목만 실제로 펼쳐지고 나머지는 닫힌다(단일 오픈).
var items = document.querySelectorAll('.pf__item');
items.forEach(function (item) {
var q = item.querySelector('.pf__q');
q.addEventListener('click', function () {
var willOpen = !item.classList.contains('is-open');
items.forEach(function (i) {
i.classList.remove('is-open');
i.querySelector('.pf__q').setAttribute('aria-expanded', 'false');
});
if (willOpen) { item.classList.add('is-open'); q.setAttribute('aria-expanded', 'true'); }
});
});import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./PromoFaq.module.scss";
export type PromoFaqProps = { duration?: number; easing?: string; color?: string };
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const ITEMS = [
{ q: "할인은 언제까지 적용되나요?", a: "마감 시각 전 결제 건까지 자동 적용됩니다." },
{ q: "환불도 되나요?", a: "결제 후 7일 이내 전액 환불됩니다." },
{ q: "연장 신청도 가능한가요?", a: "마감 전 재접속하면 자동으로 연장 안내가 뜹니다." },
];
export default function PromoFaq({ duration = 300, easing = "cubic-bezier(.2,.8,.2,1)", color = "#2f6df6" }: PromoFaqProps) {
const [open, setOpen] = useState(0);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.pf} style={vars}>
<ul className={styles.list}>
<span className={styles.scan} aria-hidden="true" />
{ITEMS.map((item, i) => (
<li key={item.q} className={`${styles.item} ${open === i ? styles.isOpen : ""}`}>
<button
type="button"
className={styles.q}
aria-expanded={open === i}
onClick={() => setOpen(open === i ? -1 : i)}
>
{item.q}
</button>
<div className={styles.a}><p>{item.a}</p></div>
</li>
))}
</ul>
</section>
);
}07 プロモFAQ — 計測 累積面積 36.616% · 動いたフレーム 17/23GODRICH オリジナル · SCSS と React は zip に同梱 · 【コピペOK】プロモ LP テンプレート9選
このデモが属する記事
【コピペOK】プロモ LP テンプレート9選プロモ LP テンプレートは、カウントダウンヒーローから申込フォームまで、キャンペーンのランディングページに必要な構成要素をひとまとめにしたコード集です。LPにそのまま貼れる九つのうち一つは、実際の目標時刻まで毎秒減っていくカウントダウンを この記事のデモをすべて見る


