GODRICH

【コピペOK】プロモ LP テンプレート9選

プロモ LP テンプレートは、カウントダウンヒーローから申込フォームまで、キャンペーンのランディングページに必要な構成要素をひとまとめにしたコード集です。LPにそのまま貼れる九つのうち一つは、実際の目標時刻まで毎秒減っていくカウントダウンを収めています。

自動再生 · タイルを押すとその項目へ · すべて1つのzip

九つは知名度順ではなく、訪問者がプロモーションページを上から読み進める順番に並べました。まず締切の時刻で足を止めさせ(カウントダウンヒーロー)、変化の実感を見せ(ビフォーアフター)、選ぶ理由を一つずつ刻み(特典ハイライト)、すでに参加した人数で背中を押し(ソーシャルプルーフ)、値引き幅を数字で見せ(割引料金)、残り枠の少なさでもう一度急かし(締切バナー)、迷いを払い(プロモFAQ)、実際に登録を受け取り(申込フォーム)、次の案内へつなげます(フッター)。担当者の顔やブランドロゴの場所に置いた図形は実物ではなく、すべてこのサイト共通の$subject-ink$subject-blue$subject-creamトークンだけで組んであります。

01カウントダウンヒーロー

ヒーロー上段には見出しとサブ見出し、下段にはカウントダウンと参加CTAが横並びで置かれます。秒の数字は実際の目標時刻まで毎秒計算し直されて短く弾み、CTAボタンは押すたびにaria-pressedが実際に反転してラベルごと切り替わります。

Date.now()transformaria-pressed
.ph__blob { animation: ph-drift 3.4s ease-in-out infinite; }
@keyframes ph-drift {
  0%, 100% { transform: translate(0, 0) scale(1); opacity: .7; }
  50%      { transform: translate(6px, -8px) scale(1.12); opacity: 1; }
}
.ph__num--s { animation: ph-tick 1s $easing infinite; }
@keyframes ph-tick {
  0%, 55%  { transform: translateY(0) scale(1); }
  78%      { transform: translateY(-3px) scale(1.1); }
  100%     { transform: translateY(0) scale(1); }
}

02ビフォーアフター

ビフォー・アフターのボタンを押すと二枚のパネルが実際にopacityでクロスフェードします。アフターパネルの上には対角線のハイライトが、パネルの状態に関係なく常にすっと流れ続けます。

opacityclickaria-pressed
// 舞台装飾: どちらのパネルが有効でも常に見えるよう、フレームの上(両パネルの兄弟要素)に置く
.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;
}
@keyframes ba-shimmer {
  0%   { transform: translateX(-30%); opacity: 0; }
  60%  { opacity: .9; }
  100% { transform: translateX(30%); opacity: 0; }
}

03特典ハイライト

特典3行のうち1行を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ソーシャルプルーフ

重なったアバター3つと「+N」バッジを合わせた4つが控えめに脈打つ間、大きな数字はページが開いた瞬間に0から実際の目標値(2,431)まで上がります。

requestAnimationFramecountertransform
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
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締切バナー

締切の進捗バーが常に控えめに揺れ続ける間、詳細を表示するボタンを押すとaria-pressedが実際に切り替わり、案内文が本当に長くなります。

scaleXclickaria-pressed
// 実際の動作: ボタンを押すとaria-pressedが切り替わり、案内文が実際に伸びる
var btn = document.querySelector('.ub__more');
var msg = document.querySelector('.ub__msg');
var base = msg.textContent;
btn.addEventListener('click', function () {
  var on = btn.getAttribute('aria-pressed') === 'true';
  btn.setAttribute('aria-pressed', String(!on));
  msg.innerHTML = on ? base : base + ' · 마감 후 정가 복귀';
});

07プロモFAQ

質問をクリックするとその項目だけが実際に開き、ほかは閉じます(単一オープン)。閉じた項目の矢印は、開閉の状態に関係なく常に控えめに揺れ続けます。

grid-template-rowsclickaria-expanded
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申込フォーム

メールアドレスを入れて申込ボタンを押すと、正規表現で実際に形式を検証します。通過するとSVGチェックがstroke-dashoffsetで描かれ、失敗すると入力欄が左右に短く揺れます。

stroke-dashoffset정규식submit
.sf__btn {
  position: relative;
  isolation: isolate;                    // z-index:-1のグローをこのコンテキストに閉じ込める
}
// 舞台装飾: ボタンの外へにじむグローリング。最初は overflow: hidden を与えたところ、
// 不透明な背景と同じ箱の中でグローがそのまま隠れて測定値が0だった。overflowを外して外へにじませた。
.sf__btn::before {
  content: "";
  position: absolute;
  inset: -10px;
  z-index: -1;
  background: radial-gradient(circle, rgba(47, 109, 246, .55), transparent 70%);
  animation: sf-glow 1.8s ease-in-out infinite;
}
@keyframes sf-glow {
  0%, 100% { opacity: .35; transform: scale(1); }
  50%      { opacity: 1; transform: scale(1.25); }
}

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を与えたところ、不透明な青いボタン背景の下にグローがそのまま隠れ、画面は普通に見えるのに実測した変化はゼロでした。overflowを外してisolation: isolateで重なりの範囲だけを区切ると、リングが表に出てきました。

ふたつ目はビフォーアフター(02)です。最初、対角線のハイライトをアフターパネルの内側に置いていましたが、そのパネルは初期状態でopacity: 0なので、ハイライトごと画面から消えていました。パネルの兄弟要素として外に出し、フレームの一番上に常時描かれるようにして解決しました。

みっつ目は特典ハイライト(03)で、性質の違う不具合に出会いました。スポットライトの位置を動かすtranslateYと、装飾用パルスのscaleXを同じ要素のtransformに同居させたところ、パルスの@keyframesが周期ごとにtransform全体を書き直し、位置が0へ巻き戻っていました。位置用の外側要素と、パルス用の内側の子要素へ二重に分けたところ直りました。計測時はいつも先頭の行が選ばれた状態だったため偶然表面化しませんでしたが、実際に別の行をクリックした瞬間に見えていたはずの不具合です。zipの中身は、この三つを直した後の最新のコードです。

アクセシビリティ

九つとも、zipを3jnnueudで開いて受け取るコードそのままの状態で、prefers-reduced-motion: reduceの環境では常時再生の装飾だけを止め、状態そのものは保ったまま表示します。カウントダウン(01)は秒カードの弾みを止めても数字の更新は続き、ソーシャルプルーフ(04)のカウントアップも情報なので変わらず動きます。特典ハイライト(03)の各行はrole="button"aria-pressedで、割引料金(05)のスイッチはrole="switch"aria-checkedで状態を伝えます。締切バナー(06)の進捗バーはrole="progressbar"aria-valuenow、プロモFAQ(07)とプロモフッター(09)はどちらもaria-expandedで開閉を知らせ、申込フォーム(08)の結果文はrole="status"(aria-live="polite")なので成功・失敗がすぐ読み上げられます。申込フォーム(08)のグローリングに使ったisolation: isolateは、MDNのisolationプロパティ解説にある通り、overflowに触れずに新しい重なりコンテキストだけを作る手段です。

ここで使った以外のランディングページ用セクションはテンプレートカテゴリーにまとめてあり、サイト運営者の情報はサイト紹介ページで確認できます。

FAQ

このテンプレートのカウントダウンは本当に動いていますか、それとも止まった画像ですか?

実際に動いています。ページを開いてからの経過時間ではなく、Date.now()で計算した目標時刻までsetInterval(1000)で毎秒数え直して表示しています。targetの計算に使う日数・時間・分・秒を実際のキャンペーン締切に合わせて差し替えるだけでそのまま使えます。

React版はCSS版と動作が違いますか?

いいえ、動作は同じです。選んだパネル・スイッチのオン/オフ・開いたFAQ項目のような状態はuseStateで保持し、変数3つ(durationeasingcolor)はpropsで受け取ってCSS変数(--duration--easing--color)へそのまま橋渡しします。ソーシャルプルーフ(04)のカウントアップもuseEffectの中で同じrequestAnimationFrameの処理をそのまま使います。

申込フォーム(08)は実際にメールを送信しますか?

いいえ、正規表現による形式チェックとSVGチェックの描画までが実際に動く範囲で、送信先のサーバーはつながっていません。submitイベントを検査するところまでがこのテンプレートの役目で、実際に使うときはその内側に本物のAPI呼び出しを足せば、検証・シェイク・チェック描画のロジックはそのまま再利用できます。

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。