GODRICH

9 Promotion Landing Page Templates — Copy-Paste

A promotion landing page template packages the sections a campaign launch runs on — countdown, proof, pricing, sign-up — and one of the nine below recalculates

Auto-plays · click a tile to jump to its section · all nine in one zip

The order below follows how a visitor actually scrolls a campaign page rather than any ranking. A countdown hero grabs attention with a deadline first, a before/after block proves the result is real, a feature highlight walks through the benefits one at a time, a social proof strip backs that up with a headcount, a discount price panel puts a number on the offer, an urgency banner presses on the deadline once more, an FAQ clears the last objection, a sign-up form collects the actual commitment, and a footer keeps the relationship going after that. Every avatar, badge, and icon shape here is drawn only from this site's own $subject-ink, $subject-blue, and $subject-cream tokens — there's no real photo, logo, or brand color anywhere in the nine, so dropping in real assets later is a straight swap that leaves the layout and the JavaScript untouched.

01Countdown hero

A headline and subhead sit above a single row that holds the countdown and its CTA button side by side, not stacked as two separate blocks. Each second card nudges up and scales for a beat as the real deadline counts down, and the CTA flips a genuine aria-pressed value and relabels itself on every click.

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);

02Before / after

Clicking either button in the role="group" toggle exclusively flips aria-pressed and cross-fades the two panels through opacity, while a diagonal highlight keeps sweeping across the top of the frame no matter which panel is showing.

opacityclickaria-pressed
// stays visible regardless of which panel is active — nesting it inside
// .ba__panel--after hid it, since that panel starts at opacity: 0
.ba__shimmer {
  position: absolute;
  inset: -20% -60%;
  z-index: 2;
  background: linear-gradient(100deg, transparent 42%, rgba(255, 247, 230, .42) 50%, transparent 58%);
  animation: ba-shimmer 2.4s linear infinite;
}

03Feature highlight

A spotlight glides between three benefit rows using a translateY value read straight from getBoundingClientRect(), auto-advancing every 1.6 seconds until a click or an Enter/Space keypress jumps it to a row and restarts that timer.

translateYsetIntervalgetBoundingClientRect
// position (JS-driven translateY) and the decorative pulse (CSS scaleX)
// can't share one element's transform, or the pulse overwrites it every cycle
.fh__spot {
  position: absolute;
  height: 40px;
  transition: transform $duration $easing;
}
.fh__spot-fill {
  transform-origin: left center;
  animation: fh-pulse 1.8s ease-in-out infinite;
}
@keyframes fh-pulse {
  0%, 100% { opacity: .4; transform: scaleX(1); }
  50%      { opacity: 1; transform: scaleX(1.02); }
}

04Social proof strip

Three overlapping avatars plus a stacked "+N" badge — four circles in total — pulse a soft box-shadow ring on staggered negative animation-delay values, while the big number beside them climbs from zero to its real target the instant the page loads. That climb runs on a cubed ease-out curve inside requestAnimationFrame, not a linear tick, and lands exactly on 2,431 with no overshoot.

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);

05Discount pricing

A role="switch" toggle flips aria-checked for real and cross-fades the monthly and yearly price panels through opacity, sliding its own knob with transform: translateX() while a "launch discount" badge keeps pulsing above the switch the whole time.

role=switchopacityclick
.dp__switch {
  width: 40px; height: 22px;
  border-radius: $r-pill;
  background: rgba(255, 247, 230, .18);
  transition: background $duration $easing;
}
.dp__switch[aria-checked="true"] { background: $subject-blue; }
.dp__knob {
  position: absolute;
  transition: transform $duration $easing;
}
.dp__switch[aria-checked="true"] .dp__knob { transform: translateX(18px); }

06Urgency banner

A role="progressbar" fill bar wobbles gently between two scaleX values while a LIVE dot blinks beside it, both running on their own regardless of any visitor action. Clicking the "details" button flips its own aria-pressed and rewrites the paragraph next to it with an added clause — the button's label never changes, only the message text does.

scaleXclickaria-pressed
.ub__dot { animation: ub-blink 1.2s ease-in-out infinite; }
@keyframes ub-blink {
  0%, 100% { opacity: 1; transform: scale(1); }
  50%      { opacity: .35; transform: scale(.8); }
}
.ub__fill {
  transform: scaleX(.82);
  transform-origin: left center;
  animation: ub-fill 2.4s $easing infinite;
}
@keyframes ub-fill {
  0%, 100% { transform: scaleX(.78); }
  50%      { transform: scaleX(.92); }
}

07Promo FAQ

A single-open accordion expands its answer through a grid-template-rows: 0fr to 1fr transition and closes whichever question was open before it. A highlight band sweeps the entire list on a constant loop, and a closed question's arrow keeps a gentle opacity nudge going until that question opens.

grid-template-rowsclickaria-expanded
.pf__scan {
  position: absolute;
  inset: 0;
  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__q::after { animation: pf-nudge 2.2s ease-in-out infinite; }

08Sign-up form

Submitting the email field runs a genuine regex check rather than pretending to validate — a pass draws a checkmark onto the button with stroke-dashoffset, a fail shakes the input left and right with translateX. A glow ring bleeds past the button's own edges the whole time, contained by isolation: isolate rather than overflow: hidden.

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 = '이메일 형식을 확인해 주세요';
  }
});

09Promo footer

The link list starts open by default, and a thin highlight line keeps sweeping left to right across the top edge no matter what state the footer is in. Clicking "expand" flips a genuine aria-expanded value and folds or unfolds the list through grid-template-rows, which is exactly what a mobile-style collapsible footer needs.

grid-template-rowsclickaria-expanded
.pt__sheen {
  position: absolute;
  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; }
}

Where it breaks — the trap

The one bug that took real debugging on this batch sat in the feature highlight (03), not the countdown or the form. Its spotlight's position comes from JavaScript writing a translateY value onto .fh__spot after every click, auto-advance tick, or keypress, and an early draft also put the decorative pulse's @keyframes — a scaleX wobble — directly on that same element. Every 1.8-second cycle, the keyframe rewrote the whole transform property from scratch and reset the position to zero, so the spotlight would snap back to row one mid-pulse and then jump forward again once the script ran. Nothing caught it in this canvas's automated capture, because the active row during that pass happened to already be row zero, making the reset and the correct value identical by coincidence — a click on row two would have shown the snap instantly. Splitting the position (.fh__spot, moved only by JS) from the pulse (.fh__spot-fill, a child carrying only the @keyframes) removed the conflict for good.

The sign-up form's glow ring (08) failed for a quieter reason. An early draft gave .sf__btn an overflow: hidden to keep its own corners tidy, which also clipped the z-index: -1 glow sitting just outside the button's box — the ring was still rendering, just entirely underneath the button's own opaque blue background, so the capture read zero. Swapping that overflow: hidden for isolation: isolate keeps the glow's stacking context contained without clipping anything meant to spill past the edge. Open the zip with 3jnnueud typed exactly as it appears here and every one of the nine matches this description, spotlight split and glow ring included.

Accessibility (reduced-motion)

Every one of the nine responds to prefers-reduced-motion: reduce by dropping its own decorative loop while keeping whatever real state it's in — the countdown hero (01) stops nudging its seconds card but keeps the digits themselves updating under a role="timer" with aria-live="off", the social proof counter (04) stops only the avatar pulse since the number climbing is information rather than decoration, the promo FAQ (07) drops its sweeping highlight band along with the closed-question arrow and the expand transition, and the sign-up form (08) forces a completed check to stay drawn at stroke-dashoffset: 0 instead of animating it in. MDN's reference on the media feature covers what a full implementation should look like.

More landing-page sections and static templates live on the template category hub, and what this site does and doesn't sell is on the about page.

FAQ

Does the countdown hero's deadline reset every time the page reloads?

Yes — the target time is Date.now() plus a fixed offset calculated the moment the script runs, not stored anywhere, so reloading always restarts the same roughly two-and-a-half-day window from a fresh "now." Wiring it to a real campaign means replacing that offset with one fixed timestamp shared across every visitor, so everyone counts down to the same deadline instead of their own load time.

Why does the discount pricing switch cross-fade the panels instead of just swapping the numbers?

Because aria-checked on the switch already tells assistive tech which plan is active without any visual cue, cross-fading through opacity mainly protects sighted users from the layout jump a straight display: none swap would cause between two price blocks of different width. Turning that transition off under prefers-reduced-motion still lands on the correct price instantly, since the state change itself carries the meaning, not the fade.

How different is the React version from the plain CSS one?

The behavior matches item for item — which panel is active, which switch is checked, which question is open, and the countdown's remaining seconds all live in useState and a useEffect interval instead of DOM classes and attributes, and the three SCSS variables (duration, easing, color) arrive as props forwarded onto CSS custom properties. The social proof counter's requestAnimationFrame loop is the one place the two diverge in substance: the React version wraps it in a useEffect that calls cancelAnimationFrame on cleanup, so navigating away mid-count can't leave a stray frame queued the way a plain script tag could.

Enter the archive password

The password is inside this article. You will find it as you read.