GODRICH

ランディングページ 改善に直結するセクション9選

ランディングページ 改善とは、訪問者がメールアドレスを残す・予約する・購入するという形で目的地まで進む割合を上げる作業です。この記事はその転換を直接受けるセクションを9つ、コードごと配ります。

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

九つは人気順ではなく、訪問者が一つずつ越えていく関門の順に並べてあります。まず「いま何人が見ているか」で関心をつかみ(01)、メールアドレスと引き換えに資料を渡して最初の行動を引き出し(02)、ウェビナーの回を押さえて約束に変え(03)、通常価格と割引価格を同じ場所に積んで財布の判断を助け(04)、残った疑いを一つずつ晴らし(05)、返金や解約の保証で不安を取り除き(06)、それでも帰ろうとする人をクーポンで引き留め(07)、読み進める人には下から出るリボンで手綱を握り(08)、最後に「無料で始める」か「デモ相談」かの二択で決めてもらいます(09)。プラン名・ファイル名・クーポンコードはすべて架空のプレースホルダーで、本物の認証マークは一つも使っていません。

01リアルタイム視聴カウンター

「いま128人が視聴中」の数字が1.2秒ごとに±1〜6ずつ揺れ、96〜168の範囲に収まったまま横の七本のバーへ順番に押し込まれていきます。バーの高さは--vに入れたscaleYの倍率そのもので、画面を見られない人へ渡す一文は5回に1回だけ書き換えます。

setIntervalscaleYaria-live
function step() {
  var d = Math.floor(Math.random() * 6) + 1;
  if (Math.random() < .5) d = -d;
  n = Math.max(96, Math.min(168, n + d));
  history.push(Math.max(.28, Math.min(1, (n - 88) / 84)));
  history.shift();
  paint();
  ticks++;
  if (ticks % 5 === 0) msg.textContent = 'いま' + n + '人がこのページを見ています';
}
setInterval(step, 1200);

02リードマグネット資料カード

メールアドレスの形式と同意チェックが両方そろった瞬間にロックが外れ、ボタンはaria-busyを立てて900ミリ秒だけ押せなくなり、ダウンロードチップを送り出します。片方でも欠けていれば入力欄が短く揺れ、受け取り済みの印はlocalStorageに残るので次の訪問では最初から完了状態で開きます。

localStoragescaleXaria-live
form.addEventListener('submit', function (e) {
  e.preventDefault();
  if (!EMAIL_RE.test(input.value) || !agree.checked) {
    input.classList.remove('is-shake');
    void input.offsetWidth;
    input.classList.add('is-shake');
    msg.textContent = 'メールアドレスと同意欄をご確認ください';
    return;
  }
  btn.disabled = true;
  btn.setAttribute('aria-busy', 'true');
  form.classList.add('is-busy');
  msg.textContent = 'ファイルを準備しています';
  setTimeout(function () {
    form.classList.remove('is-busy');
    btn.disabled = false;
    btn.removeAttribute('aria-busy');
    try { localStorage.setItem('lead271', '1'); } catch (e2) {}
    finish();
  }, 900);
});

03ウェビナー回次予約セクション

三つの回のうち満席の回はdisabledなので最初から押せず、左右の方向キーはその回を飛ばして次の空き回へ移ります。空き回を選ぶと受付終了までの残り時間が秒単位で数字の帯を回し、予約ボタンを押すとその枠に予約済みのバッジが降り着きます。

aria-checkedtranslateYsetInterval
function pick(i) {
  picked = i;
  slots.forEach(function (b, j) {
    b.setAttribute('aria-checked', j === i ? 'true' : 'false');
    b.tabIndex = j === i ? 0 : -1;
  });
  slots[i].focus({ preventScroll: true });
}
document.getElementById('wb-slots').addEventListener('keydown', function (e) {
  if (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft') return;
  e.preventDefault();
  var n = picked, step = e.key === 'ArrowRight' ? 1 : slots.length - 1;
  do { n = (n + step) % slots.length; } while (slots[n].disabled && n !== picked);
  pick(n);
});

04価格アンカー比較スタック

内訳を開くと、定価・割引価格・クーポン適用後・お得額の四行が80ミリ秒ずつずれて着地し、定価には打ち消し線が左から走ります。ずれは--iという行番号の変数をanimation-delayに掛けただけなので、行を増やしても遅延の計算式は変えずに済みます。

animation-delayscaleXaria-expanded
.stage.is-demo .ap__row {
  animation-name: ap-rise;
  animation-duration: $duration;
  animation-timing-function: $easing;
  animation-iteration-count: infinite;
  animation-fill-mode: backwards;
  animation-delay: calc(var(--i) * 80ms);
}
@keyframes ap-rise {
  0%        { opacity: 0; transform: translateY(10px); }
  12%       { opacity: 1; transform: translateY(0); }
  74%       { opacity: 1; transform: translateY(0); }
  78%, 100% { opacity: 0; transform: translateY(10px); }
}

05懸念解消FAQと到達CTA

価格・お試し・解約という三つのためらいを開いて答えを読むと、その行に「解消」の印が付きます。三つ全部を一度ずつ開いた瞬間だけis-readyが立ち、下のボタンが大きくなって文字が入れ替わり、role="status"の一文が同時に流れます。

grid-template-rowsaria-expandedscale
items.forEach(function (item, idx) {
  var q = item.querySelector('.ob__q');
  q.addEventListener('click', function () {
    var open = item.classList.toggle('is-open');
    q.setAttribute('aria-expanded', String(open));
    if (open && opened.indexOf(idx) < 0) opened.push(idx);
    if (opened.length === items.length && !ob.classList.contains('is-ready')) {
      ob.classList.add('is-ready');
      label.setAttribute('aria-hidden', 'true');
      labelReady.removeAttribute('aria-hidden');
      status.textContent = '気がかりはすべて解消しました。';
    }
  });
});

06保証バッジセクション

返金保証・安全な決済・データ保護・いつでも解約の四枚が、100ミリ秒ずつ遅れてスタンプのように降り着きます。盾の中のチェックだけは線が描かれて現れる仕掛けで、stroke-dasharrayと同じ26という長さをstroke-dashoffsetに往復させています。

stroke-dashoffsetaria-expandedscale
.gb__draw polyline { stroke-dasharray: 26; stroke-dashoffset: 0; }

.stage.is-demo .gb__draw polyline {
  animation-name: gb-draw;
  animation-duration: $duration;
  animation-timing-function: $ease-out;
  animation-iteration-count: infinite;
  animation-fill-mode: backwards;
  animation-delay: 160ms;
}
@keyframes gb-draw {
  0%        { stroke-dashoffset: 26; }
  34%, 70%  { stroke-dashoffset: 0; }
  74%, 100% { stroke-dashoffset: 26; }
}

07離脱意向クーポンポップアップ

カーソルが画面の上端から12px以内で外へ抜けた瞬間だけクーポンカードが立ち上がり、閉じるボタンへフォーカスが移ります。コードのコピーとEscapeでの復帰はその場で動き、同じセッションでは二度と開かないという約束までsessionStorageの一行で実際に守られます。

sessionStoragetranslateYaria-modal
function seen() { try { return sessionStorage.getItem('exit271') === '1'; } catch (err) { return false; } }
function openPop() {
  pop.classList.add('is-open');
  try { sessionStorage.setItem('exit271', '1'); } catch (err) {}
  closeBtn.focus({preventScroll:true});
}
function closePop() {
  pop.classList.remove('is-open');
  msg.textContent = MICRO;
  cta.focus({preventScroll:true});
}
document.addEventListener('mouseleave', function (e) {
  if (e.clientY < 12 && !pop.classList.contains('is-open')) {
    if (seen()) { msg.textContent = MICRO; return; }
    openPop();
  }
});
document.addEventListener('keydown', function (e) {
  if (e.key === 'Escape' && pop.classList.contains('is-open')) closePop();
});

08スクロール到達CTAリボン

決め手になる段落が枠の中で6割以上見えた瞬間、下の端からリボンが上がってきます。見張り役はIntersectionObserverで、rootをデモのスクロール枠に指定してあるため、ページ全体ではなく枠の中の読み進みだけを数えます。閉じると同じ場所に再表示のピルが残り、その状態はsessionStorageに書かれます。

IntersectionObservertranslateYsessionStorage
var io = new IntersectionObserver(function (entries) {
  if (entries[0].isIntersecting && !closed) {
    ribbon.classList.add('is-up');
    msg.textContent = '開始バナーが表示されました';
  }
}, { root: vp, threshold: 0.6 });
io.observe(mark);

closeBtn.addEventListener('click', function () {
  closed = true;
  ribbon.classList.remove('is-up');
  again.hidden = false;
  again.focus({preventScroll:true});
  msg.textContent = 'バナーを閉じています';
  try { sessionStorage.setItem('ribbon271', 'closed'); } catch (err) {}
});

09最終CTA二択

「無料で始める」と「デモ相談」は本物のradioなので、クリックでも左右の方向キーでも選べます。選んだ側が前に出て、下の一行が「次の一手」のプレビューに入れ替わり、Enterかダブルクリックで開始ボタンへ変わります。状態の書き換えはpaint()一つに集めてあり、案内文の切り替えも同じ関数の中で一緒に済ませます。

aria-checkedscaletranslateX
function paint() {
  a.setAttribute('aria-checked', String(a.checked));
  b.setAttribute('aria-checked', String(b.checked));
  pv.classList.toggle('is-b', b.checked);
  pv.classList.toggle('is-armed', armed);
  pvA.setAttribute('aria-hidden', String(armed || b.checked));
  pvB.setAttribute('aria-hidden', String(armed || a.checked));
  pvGo.setAttribute('aria-hidden', String(!armed));
  msg.classList.toggle('is-b', b.checked);
  msgA.setAttribute('aria-hidden', String(b.checked));
  msgB.setAttribute('aria-hidden', String(a.checked));
}
paint();
group.addEventListener('change', function () { armed = false; paint(); });
group.addEventListener('dblclick', function () { armed = true; paint(); });

九つを480×300で24フレーム撮って測った値が下の表です。累積面積は撮影中に画素が入れ替わった割合の合計、フレーム最大は一枚のフレームで最も大きく変わった割合で、数字が小さいほど静かな動きです。

番号 セクション 越える関門 累積面積 % フレーム最大 % 強度 動いたフレーム
01 リアルタイム視聴カウンター 関心 3.313 0.581 144.1 23/24
02 リードマグネット資料カード 最初の行動 13.067 6.982 60.5 15/24
03 ウェビナー回次予約セクション 約束 3.188 1.799 74.8 14/24
04 価格アンカー比較スタック 財布 4.728 2.149 92.4 13/24
05 懸念解消FAQと到達CTA 疑い 40.843 15.855 118.4 22/24
06 保証バッジセクション 安心 8.107 3.490 42.6 17/24
07 離脱意向クーポンポップアップ 引き留め 23.675 17.151 90.0 20/24
08 スクロール到達CTAリボン 手綱 21.055 9.894 125.5 20/24
09 最終CTA二択 決定 26.074 23.622 61.9 11/24

どこで壊れるか — 落とし穴

一つ目は最終CTA二択(09)で、代表画像が嘘をついた場面です。選択の見た目はsteps(1, end)のコマ送りで切り替わるのに、role="status"の案内文はページが開いた一度きりしか書き込まれていませんでした。そのせいで、プレビューの周回から選ばれた代表フレームは「デモ相談」が前に出ている絵なのに、文だけが「無料で始めるを選びました」のまま焼き付いていたのです。案内文を二行重ねて置き、選択と同じ時計、つまり同じコマのキーフレームで入れ替わるようにして直しました。

二つ目は懸念解消FAQと到達CTA(05)の閉じる順番です。三行をanimation-delay: calc(var(--i) * 160ms)でずらして開いたところ、閉じるときにも同じずれが効いてしまい、一行目だけが閉じている区間が生まれました。その区間でもCTAのラベルは「準備完了」のままだったので、「一行目はまだ解けていないのにボタンだけ武装している」フレームが代表画像に選ばれてしまったのです。ラベルが変わっている区間を三行すべて開いている41〜64%へ狭め、閉じる動きを開く動きよりずっと遅く66%から100%へ伸ばして解決しました。ゆっくり伸ばした分だけ一フレームあたりの変化面積も下がるので、閉じる場面が代表画像に拾われなくなりました。

三つ目はスクロール到達CTAリボン(08)で、リボンなしで案内文だけが出た画面です。プレビューの周回では、IntersectionObserverが発火した途端に文が出るのに、リボンはtranslateY(110%)でまだ下に隠れたままでした。文をリボンと同じ41%の時点でコマ切り替えにし、読み進める区間を34〜86%へ長く伸ばして一フレームあたりの変化を下げると、代表画像が「リボンが上がってきた瞬間」に変わりました。なお、スクロールはoverflow-y: autoの箱ではなく、その中のトラックにだけtransformを掛けます。箱そのものに掛けると、スクロール位置が丸ごとずれます。プレビューは2秒しか撮れないため、4秒のループを組むと後半が永久に写りません。07・08・09のループを2秒へ縮めたのはそのためです。

アクセシビリティ

画面を見ずに使う人にも、九つの状態はそれぞれ別の経路で届きます。リアルタイム視聴カウンター(01)の要約文はrole="status"ですが、更新は5ティックに1回だけに絞ってあり、バーはtransform: scaleY(var(--v))で動く飾りとして読み上げから外しています。リードマグネット資料カード(02)は<label for>で見出しと入力欄を結び、送信中のボタンにaria-busyを立て、結果はrole="status"で伝えます。ウェビナー回次予約セクション(03)のカウントダウンはrole="timer"aria-live="off"を添えて毎秒の読み上げを止め、満席の回はdisabled、選択中の回はaria-checkedで、左右の方向キーが満席の回を飛ばして動きます。価格アンカー比較スタック(04)・懸念解消FAQと到達CTA(05)・保証バッジセクション(06)の開閉は、どれもaria-expandedgrid-template-rows: 0fr → 1frの組み合わせです。離脱意向クーポンポップアップ(07)はrole="dialog" aria-modal="true"で、開いた瞬間にfocus({ preventScroll: true })で閉じるボタンへ焦点が移り、Escapeキーで元の場所へ戻れます。スクロール到達CTAリボン(08)の閉じるボタンにはaria-labelが付き、再表示ボタンは見える文字がそのまま名前になり、閉じた事実はsessionStorageに残ります。最終CTA二択(09)は本物のradiorole="radiogroup"で包んだだけなので、方向キーの移動はブラウザーが自前で面倒を見ます。配布しているzipをfuvny8y4で開くと、この九つは@media (prefers-reduced-motion: reduce)の宣言に!importantを付けて動きだけを止め、選択・開閉・残り時間といった状態は画面にそのまま残す形で入っています。この判定の考え方はMDNのprefers-reduced-motionの解説が出発点で、到達の見張りに使ったMDNのIntersection Observer APIも同じ場所で読めます。

同じ設計で信頼側を受け持つ姉妹記事は信頼を作るLPパーツ9選にあります。ほかのテンプレートはテンプレートの一覧に並べてあり、運営者の経歴はこのサイトについてにまとめました。

FAQ

離脱意向クーポンポップアップ(07)は本当にカーソルの動きで出ますか?

documentmouseleaveを監視し、clientYが12px未満のときだけ開くので、画面の上端へ抜ける動きにだけ反応します。横や下へ外れたときは開きません。一度出た事実はsessionStorageexit271に残るため、同じセッションで二度目は開かず、代わりに小さな案内文だけがその場に残ります。

スクロール到達CTAリボン(08)はページ全体をスクロールしなくても試せますか?

はい。IntersectionObserverのrootをデモの中のスクロール枠に指定してあるので、枠の中を読み進めるだけでthreshold: 0.6を超え、リボンが上がります。実際のページへ移すときはrootnullに戻せば、そのままウィンドウのスクロールで同じように動きます。

React版もCSS版と同じように動きますか?

はい。開閉・選択・到達といった状態はすべてuseStateに移し、durationeasingcolorの三つはpropsで受け取ってCSS変数へ渡します。カウンター(01)のsetIntervalと離脱検知(07)のmouseleaveuseEffectの中で登録し、後片付けも同じ場所で行うので、画面を離れてもタイマーが残りません。

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

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