09프로모 푸터
'바로가기'를 누르면 aria-expanded가 바뀌며 링크 목록이 grid-template-rows로 실제로 접히고 펼쳐지고, 상단 얇은 하이라이트 선이 계속 좌우로 스칩니다.
쓰임프로모션 랜딩 하단 푸터, 모바일형 접이식 링크 모음
grid-template-rowsclickaria-expanded

<main class="stage">
<footer class="pt">
<span class="pt__sheen" aria-hidden="true"></span>
<div class="pt__top">
<span class="pt__mark" aria-hidden="true"></span>
<p class="pt__tagline">캠페인이 끝나도 계속 씁니다</p>
</div>
<div class="pt__group">
<button type="button" class="pt__group-toggle" aria-expanded="true">바로가기</button>
<div class="pt__links-wrap is-open">
<ul class="pt__links">
<li><a href="#">요금제</a></li>
<li><a href="#">이용약관</a></li>
<li><a href="#">문의하기</a></li>
</ul>
</div>
</div>
</footer>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-base; // 링크 목록 펼침
$easing: $ease-spring;
$color: $subject-blue;
@include stage($stage-ink);
.pt {
position: relative;
width: min(400px, 96vw);
padding-top: $sp-4;
overflow: hidden;
}
.pt__sheen {
position: absolute;
top: 0; left: 0; right: 0;
height: 2px;
background: linear-gradient(90deg, transparent, $subject-blue, transparent);
animation: pt-sheen 2.6s linear infinite;
}
@keyframes pt-sheen {
0% { transform: translateX(-100%); opacity: 0; }
15% { opacity: 1; }
85% { opacity: 1; }
100% { transform: translateX(100%); opacity: 0; }
}
.pt__top { display: flex; align-items: center; gap: $sp-2; margin-bottom: $sp-3; }
.pt__mark { width: 22px; height: 22px; border-radius: $r-sm; background: $subject-blue; }
.pt__tagline { margin: 0; font-weight: 600; font-size: 11px; line-height: 1.3; font-family: inherit; color: rgba(255, 247, 230, .55); }
.pt__group { border-top: 1px solid rgba(255, 247, 230, .12); padding-top: $sp-2; }
.pt__group-toggle {
display: block;
width: 100%;
box-sizing: border-box;
text-align: left;
border: 0;
cursor: pointer;
padding: $sp-1 0;
background: transparent;
color: $subject-cream;
font-weight: 700; font-size: 11px; line-height: 1; font-family: inherit;
letter-spacing: .04em;
}
.pt__links-wrap {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows $duration $easing;
}
.pt__links-wrap.is-open { grid-template-rows: 1fr; }
.pt__links { list-style: none; margin: 0; padding: 0; overflow: hidden; }
.pt__links li { padding-top: $sp-2; }
.pt__links a {
color: rgba(255, 247, 230, .68);
text-decoration: none;
font-weight: 500; font-size: 12px; line-height: 1.6; font-family: inherit;
}
// ── 접근성 ──────────────────────────────────────────
@media (prefers-reduced-motion: reduce) {
.pt__sheen { animation: none !important; opacity: 0 !important; }
.pt__links-wrap { transition: none !important; }
}
// ── 반응형(320×200) ──────────────────────────────────
@media (max-width: 340px), (max-height: 220px) {
.pt__tagline, .pt__mark { display: none; }
.pt { padding-top: $sp-2; }
.pt__top { margin-bottom: $sp-2; }
.pt__group { padding-top: $sp-1; }
.pt__links li { padding-top: $sp-1; }
.pt__links a { font-size: 11px; }
}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);
}
.pt {
position: relative;
width: min(400px, 96vw);
padding-top: 16px;
overflow: hidden;
}
.pt__sheen {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg, transparent, #2f6df6, transparent);
animation: pt-sheen 2.6s linear infinite;
}
@keyframes pt-sheen {
0% {
transform: translateX(-100%);
opacity: 0;
}
15% {
opacity: 1;
}
85% {
opacity: 1;
}
100% {
transform: translateX(100%);
opacity: 0;
}
}
.pt__top {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.pt__mark {
width: 22px;
height: 22px;
border-radius: 8px;
background: #2f6df6;
}
.pt__tagline {
margin: 0;
font-weight: 600;
font-size: 11px;
line-height: 1.3;
font-family: inherit;
color: rgba(255, 247, 230, 0.55);
}
.pt__group {
border-top: 1px solid rgba(255, 247, 230, 0.12);
padding-top: 8px;
}
.pt__group-toggle {
display: block;
width: 100%;
box-sizing: border-box;
text-align: left;
border: 0;
cursor: pointer;
padding: 4px 0;
background: transparent;
color: #fff7e6;
font-weight: 700;
font-size: 11px;
line-height: 1;
font-family: inherit;
letter-spacing: 0.04em;
}
.pt__links-wrap {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.pt__links-wrap.is-open {
grid-template-rows: 1fr;
}
.pt__links {
list-style: none;
margin: 0;
padding: 0;
overflow: hidden;
}
.pt__links li {
padding-top: 8px;
}
.pt__links a {
color: rgba(255, 247, 230, 0.68);
text-decoration: none;
font-weight: 500;
font-size: 12px;
line-height: 1.6;
font-family: inherit;
}
@media (prefers-reduced-motion: reduce) {
.pt__sheen {
animation: none !important;
opacity: 0 !important;
}
.pt__links-wrap {
transition: none !important;
}
}
@media (max-width: 340px), (max-height: 220px) {
.pt__tagline, .pt__mark {
display: none;
}
.pt {
padding-top: 8px;
}
.pt__top {
margin-bottom: 8px;
}
.pt__group {
padding-top: 4px;
}
.pt__links li {
padding-top: 4px;
}
.pt__links a {
font-size: 11px;
}
}// 실제 동작: "바로가기" 를 누르면 aria-expanded 가 바뀌고 링크 목록이 실제로 접히고 펼쳐진다(모바일 푸터 패턴).
var toggle = document.querySelector('.pt__group-toggle');
var links = document.querySelector('.pt__links-wrap');
toggle.addEventListener('click', function () {
var open = toggle.getAttribute('aria-expanded') === 'true';
toggle.setAttribute('aria-expanded', String(!open));
links.classList.toggle('is-open', !open);
});import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./PromoFooter.module.scss";
export type PromoFooterProps = { duration?: number; easing?: string; color?: string };
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function PromoFooter({ duration = 300, easing = "cubic-bezier(.2,.8,.2,1)", color = "#2f6df6" }: PromoFooterProps) {
const [open, setOpen] = useState(true);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<footer className={styles.pt} style={vars}>
<span className={styles.sheen} aria-hidden="true" />
<div className={styles.top}>
<span className={styles.mark} aria-hidden="true" />
<p className={styles.tagline}>캠페인이 끝나도 계속 씁니다</p>
</div>
<div className={styles.group}>
<button type="button" className={styles.groupToggle} aria-expanded={open} onClick={() => setOpen((v) => !v)}>
바로가기
</button>
<div className={`${styles.linksWrap} ${open ? styles.isOpen : ""}`}>
<ul className={styles.links}>
<li><a href="#">요금제</a></li>
<li><a href="#">이용약관</a></li>
<li><a href="#">문의하기</a></li>
</ul>
</div>
</div>
</footer>
);
}09 프로모 푸터 — 측정 누적면적 0.556% · 움직인 프레임 22/23GODRICH 원작 · zip 에 SCSS·React 포함 · 프로모션 페이지 랜딩 섹션 템플릿 9종
이 데모가 속한 글
프로모션 페이지 랜딩 섹션 템플릿 9종프로모션 페이지 섹션 템플릿은 카운트다운 히어로부터 신청 폼까지 캠페인 랜딩에 필요한 구성 요소를 통째로 담은 코드 모음입니다. 랜딩 페이지에 바로 붙이는 아홉 개 중 하나는 실제 목표 시각까지 매초 줄어드는 카운트 이 글의 데모 전부 보기


