GODRICH

프로모션 페이지 랜딩 섹션 템플릿 9종

프로모션 페이지 섹션 템플릿은 카운트다운 히어로부터 신청 폼까지 캠페인 랜딩에 필요한 구성 요소를 통째로 담은 코드 모음입니다. 랜딩 페이지에 바로 붙이는 아홉 개 중 하나는 실제 목표 시각까지 매초 줄어드는 카운트다운을 담았습니다.

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

아홉 개는 인기순이 아니라 방문자가 프로모션 페이지를 훑는 순서대로 놓았습니다. 먼저 마감 시각으로 눈길을 붙잡고(카운트다운 히어로), 달라진 결과를 보여주고(비포·애프터), 핵심 혜택을 하나씩 각인시키고(혜택 하이라이트), 이미 참여한 사람 수로 신뢰를 더하고(소셜 프루프), 할인 폭을 숫자로 보여주고(할인 가격표), 남은 자리로 다시 한 번 재촉하고(마감 배너), 망설임을 없애고(FAQ), 실제로 등록받고(신청 폼), 다음 안내로 이어갑니다(푸터). 담당자 사진·실제 로고 자리는 전부 이 사이트의 먹·파랑·크림 토큰만으로 그린 도형 플레이스홀더라, 사진 파일만 바꿔 끼워도 격자·정렬은 그대로 유지됩니다.

01카운트다운 히어로

히어로 배너 위쪽엔 헤드라인과 부제가, 아래쪽 줄엔 카운트다운과 CTA 버튼이 나란히 놓입니다. 초 단위 숫자는 실제 목표 시각까지 매초 줄어들며 짧게 넛지되고, CTA 버튼은 누를 때마다 aria-pressed가 실제로 바뀝니다.

Date.now()transformaria-pressed
var target = Date.now() + ((2 * 24 * 60 * 60) + (14 * 60 * 60) + (36 * 60) + 9) * 1000;
function tick() {
  var diff = Math.max(0, target - Date.now());
  var s = Math.floor(diff / 1000);
  els.d.textContent = pad(Math.floor(s / 86400));
  els.h.textContent = pad(Math.floor((s % 86400) / 3600));
  els.m.textContent = pad(Math.floor((s % 3600) / 60));
  els.s.textContent = pad(s % 60);
}
tick();
setInterval(tick, 1000);

02비포·애프터

비포·애프터 버튼을 클릭하면 두 패널이 실제로 opacity 크로스페이드됩니다. 애프터 패널 위로는 대각선 하이라이트가 계속 스치듯 흐릅니다.

opacityclickaria-pressed
var btns = document.querySelectorAll('.ba__btn');
var panels = document.querySelectorAll('.ba__panel');
btns.forEach(function (btn) {
  btn.addEventListener('click', function () {
    var name = btn.getAttribute('data-panel');
    btns.forEach(function (b) { b.setAttribute('aria-pressed', String(b === btn)); });
    panels.forEach(function (p) { p.classList.toggle('is-active', p.getAttribute('data-panel') === name); });
  });
});

03핵심 혜택 하이라이트

핵심 혜택 세 줄 중 하나를 1.6초마다 스포트라이트가 자동으로 옮겨가며 비춥니다. 행을 클릭하거나 Enter를 누르면 그 자리로 실측한 높이만큼 바로 이동합니다.

translateYsetIntervalgetBoundingClientRect
function go(i) {
  current = i;
  rows.forEach(function (r, idx) {
    var on = idx === i;
    r.classList.toggle('is-active', on);
    r.setAttribute('aria-pressed', String(on));
  });
  var offset = rows[i].getBoundingClientRect().top - rows[0].getBoundingClientRect().top;
  spot.style.transform = 'translateY(' + offset + 'px)';
}

04소셜 프루프 띠

겹친 아바타 세 개와 "+N" 배지까지 총 네 개가 은은하게 맥박치는 동안, 큰 숫자는 페이지를 열자마자 0에서 실제 목표값(2,431)까지 올라갑니다.

requestAnimationFramecountertransform
var el = document.querySelector('.sp__num');
var target = Number(el.getAttribute('data-target'));
var start = null;
var DUR = 1200;
function frame(ts) {
  if (!start) start = ts;
  var p = Math.min(1, (ts - start) / DUR);
  var eased = 1 - Math.pow(1 - p, 3);
  el.textContent = Math.round(eased * target).toLocaleString('ko-KR');
  if (p < 1) requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

05할인 가격표

연간 결제 스위치를 누르면 aria-checked가 실제로 바뀌면서 월간·연간 가격판이 크로스페이드됩니다. 원가에는 취소선이 그대로 남습니다.

role=switchopacityclick
var sw = document.querySelector('.dp__switch');
var monthly = document.querySelector('.dp__price--monthly');
var yearly = document.querySelector('.dp__price--yearly');
sw.addEventListener('click', function () {
  var on = sw.getAttribute('aria-checked') === 'true';
  sw.setAttribute('aria-checked', String(!on));
  monthly.classList.toggle('is-active', on);
  yearly.classList.toggle('is-active', !on);
});

06마감 임박 배너

마감 임박 진행바가 계속 은은하게 출렁이는 동안, "자세히" 버튼을 누르면 안내 문구가 실제로 늘어납니다.

scaleXclickaria-pressed
.ub__fill {
  transform: scaleX(.78);
  transform-origin: left center;
  animation: ub-fill 2.4s $easing infinite;
}
@keyframes ub-fill {
  0%, 100% { transform: scaleX(.78); }
  50%      { transform: scaleX(.92); }
}

07프로모 FAQ

질문을 클릭하면 그 항목만 실제로 펼쳐지고 나머지는 접힙니다. 닫힌 항목의 화살표는 계속 은은하게 흔들립니다.

grid-template-rowsclickaria-expanded
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'); }
  });
});

08신청 폼

이메일을 넣고 신청 버튼을 누르면 정규식으로 실제 형식을 검사합니다. 통과하면 체크가 그려지고, 틀리면 입력칸이 좌우로 짧게 흔들립니다.

stroke-dashoffset정규식submit
var RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
form.addEventListener('submit', function (e) {
  e.preventDefault();
  if (RE.test(input.value)) {
    btn.classList.remove('is-error');
    btn.classList.add('is-done');
    status.textContent = '신청 완료 — 코드가 곧 도착합니다';
  } else {
    btn.classList.remove('is-done');
    input.classList.remove('shake'); void input.offsetWidth;
    input.classList.add('shake');
    status.textContent = '이메일 형식을 확인해 주세요';
  }
});

09프로모 푸터

"바로가기"를 누르면 링크 목록이 실제로 접히고 펼쳐집니다. 상단 얇은 하이라이트 선은 계속 좌우로 스칩니다.

grid-template-rowsclickaria-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);
});

어디서 깨지는가 — 함정

아홉 개를 만드는 동안 실제로 같은 계열의 병에 두 번 걸렸습니다. 신청 폼(08) 버튼엔 바깥으로 번지는 글로우 링을 뒀는데, 여기에 overflow: hidden을 걸었더니 글로우가 버튼 자신의 불투명한 파란 배경 밑에 그대로 가려졌습니다. 화면은 멀쩡해 보였지만 측정값은 누적면적 0%였습니다. overflow를 없애고 isolation: isolate로 위치만 가두자 링이 살아났습니다.

비포·애프터(02)도 비슷했습니다. 처음엔 대각선 하이라이트를 애프터 패널 안에 넣었는데, 그 패널이 기본값으로 opacity 0이라 하이라이트까지 함께 숨어 있었습니다. 패널의 형제로 빼 프레임 맨 위에 항상 그리게 하자 해결됐습니다.

핵심 혜택 하이라이트(03)에서는 다른 종류의 함정을 만났습니다. 스포트라이트 위치를 옮기는 translateY와 장식용 펄스의 scaleX를 같은 요소의 transform에 같이 썼더니, 펄스 keyframe이 매 주기 transform 전체를 새로 써 위치가 0으로 리셋됐습니다. zip 안 코드는 이 문제를 겪은 뒤 위치용 바깥 요소와 펄스용 안쪽 요소 두 겹으로 나눈 최신 버전입니다. 측정 때는 첫 번째 행이 항상 활성 상태라 우연히 드러나지 않았지만, 실제로 다른 행을 클릭하는 순간 바로 보였을 문제였습니다.

접근성

아홉 개 전부, zip을 3jnnueud으로 열어 받는 코드 그대로 prefers-reduced-motion: reduce에서 장식 애니메이션을 끄되 상태 자체는 남깁니다 — 카운트다운(01)은 숫자 넛지만 끄고 갱신은 유지하고, 소셜 프루프(04)의 카운트업도 정보이므로 그대로 유지합니다. 할인 가격표(05)의 스위치는 role="switch"aria-checked로, 마감 배너(06)의 진행바는 role="progressbar"aria-valuenow로 상태를 전달합니다. 프로모 FAQ(07)와 프로모 푸터(09)는 둘 다 aria-expanded로 펼침 상태를 알리고, 신청 폼(08)의 결과 문구는 role="status"(aria-live="polite")라 스크린 리더가 성공·실패를 바로 읽습니다. 펼침 애니메이션에 쓴 grid-template-rows: 0fr → 1fr 전환은 MDN의 grid-template-rows 문서에 나온 표준 트랙 크기 문법을 그대로 씁니다.

이벤트·프레스룸 섹션이 궁금하면 정적 템플릿 모음을 함께 보시고, 이 사이트를 만드는 사람 정보는 사이트 소개 페이지에 있습니다.

FAQ

이 템플릿의 카운트다운은 실제로 작동하나요, 아니면 정지 이미지인가요?

네, 실제로 작동합니다. Date.now()로 잰 목표 시각까지 setInterval(1000)으로 매초 다시 계산해 표시합니다. 목표 시각과 초기 표시값만 실제 캠페인 마감 시각으로 바꾸면 그대로 쓸 수 있습니다.

React 버전은 CSS 버전과 동작이 다른가요?

아니요, 동작은 같습니다. 활성 패널·스위치 상태·펼침 여부 같은 값은 useState로 관리하고, 변수 3개(duration·easing·color)는 props로 받아 CSS 변수로 그대로 넘깁니다. 카운트다운(01)의 초 단위 갱신도 useEffect 안에서 같은 setInterval 로직을 씁니다.

신청 폼(08)이 실제로 이메일을 어딘가로 보내나요?

아니요, 데모 자체는 형식 검사와 성공·실패 표시까지만 실제로 동작하고 실제 전송 서버는 연결돼 있지 않습니다. formsubmit 이벤트를 가로채 정규식으로 검사하는 부분까지가 이 템플릿의 역할이고, 실제로 쓸 때는 그 자리에 실제 API 호출만 추가하면 나머지 검증·표시 로직은 그대로 재사용할 수 있습니다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.