「デザインカンプ通りに組んだのに、スマホで見ると崩れている」。LPの制作現場で、こんな経験をしたことがある方は多いのではないでしょうか。デスクトップ向けに作られたデザインをそのまま実装して、後からスマホ対応を付け足す——この進め方が、レスポンシブ崩れの大きな原因になっています。

最近のアクセス解析を見ると、多くのLPでモバイルからのアクセスが60〜80%を占めています。にもかかわらず、実装の出発点がデスクトップのままでは、モバイルユーザーにとって最適な体験を提供するのは難しいものです。

この記事では、CSS GridやContainer Queriesといった現代のCSS仕様を使って、モバイルファーストでLPを実装するパターンを紹介します。コピー&ペーストで使えるコード例を中心に、実務で「すぐ使える」内容にまとめました。

モバイルファーストで書くLPのCSS設計

デスクトップファーストの何が問題なのか

従来のLP実装では、まずデスクトップ向けのレイアウトを組み、max-widthのメディアクエリでスマホ向けに「縮める」アプローチが主流でした。しかしこの方法には、いくつかの構造的な問題があります。

まず、デスクトップ用のCSSがベースになるため、モバイルでは不要なスタイルを上書きで打ち消す記述が増えます。CSSの量が膨らみ、メンテナンスが複雑になります。さらに、モバイル向けの調整が「後付け」になるため、レイアウト崩れの温床になりやすいのです。

モバイルファーストのアプローチは逆です。まずモバイル向けのシンプルなレイアウトをベースに書き、min-widthのメディアクエリで画面幅に応じてレイアウトを拡張していきます。

/* ベース: モバイル向け(単一カラム) */
.lp-section {
  padding: 3rem 1rem;
}

/* タブレット以上 */
@media (min-width: 768px) {
  .lp-section {
    padding: 4rem 2rem;
  }
}

/* デスクトップ */
@media (min-width: 1024px) {
  .lp-section {
    padding: 5rem 3rem;
    max-width: 1200px;
    margin: 0 auto;
  }
}

こうすることで、モバイルのCSSはシンプルなまま保たれ、画面が大きくなるにつれてレイアウトが拡張されていきます。GoogleのPageSpeed Insightsでもモバイルの評価が先に表示されるように、検索エンジンもモバイルファーストを前提にしています。

ブレイクポイントの設計指針

LPのブレイクポイントは、サイト全体で使うものとは少し考え方が異なります。LPはページ単位で完結するため、コンテンツの見え方に合わせてブレイクポイントを決めるのが効果的です。

実務で使いやすいのは、以下の3段階です。

/* 推奨ブレイクポイント */
:root {
  /* 〜767px:  モバイル(ベース) */
  /* 768px〜:  タブレット・小型PC */
  /* 1024px〜: デスクトップ */
}

ただし「768pxでタブレット」と機械的に決めるのではなく、実際のコンテンツが2カラムになった時に読みやすいか、カード型のレイアウトが3列になった時にカードの幅は十分かといった視点で調整してください。

CSS Gridで LP のセクションレイアウトを組む

ヒーローセクションの実装

LPの第一印象を決めるヒーローセクションは、デバイスによってレイアウトを大きく変えたい箇所です。CSS Gridを使うと、HTMLの構造を変えずにレイアウトを柔軟に切り替えられます。

<section class="hero">
  <div class="hero__content">
    <h1 class="hero__title">月額3万円から始める<br>Web集客の自動化</h1>
    <p class="hero__subtitle">御社の営業チームが商談に集中できる仕組みを、3ヶ月で構築します</p>
    <a href="#contact" class="hero__cta">無料相談はこちら</a>
  </div>
  <div class="hero__visual">
    <picture>
      <source srcset="/images/hero-desktop.webp" media="(min-width: 1024px)" type="image/webp">
      <source srcset="/images/hero-mobile.webp" type="image/webp">
      <img src="/images/hero-mobile.jpg" alt="ダッシュボード画面" width="800" height="600" loading="eager">
    </picture>
  </div>
</section>
.hero {
  display: grid;
  gap: 2rem;
  padding: 3rem 1rem;
  align-items: center;
}

/* モバイル: コンテンツ → 画像の順で縦積み */
.hero__content { order: 1; }
.hero__visual  { order: 2; }

.hero__title {
  font-size: clamp(1.75rem, 5vw, 3rem);
  line-height: 1.3;
  font-weight: 700;
}

/* デスクトップ: 左右2カラム */
@media (min-width: 1024px) {
  .hero {
    grid-template-columns: 1fr 1fr;
    padding: 5rem 3rem;
    max-width: 1200px;
    margin: 0 auto;
  }
}

ポイントはclamp()関数を使ったフルイドタイポグラフィです。clamp(1.75rem, 5vw, 3rem)と書くことで、画面幅に応じてフォントサイズが滑らかに変化し、メディアクエリを使わずにレスポンシブな文字サイズを実現できます。W3CのCSS Values and Units仕様で定義されているこの関数は、主要ブラウザでほぼ100%サポートされています。

特徴紹介セクション(カード型レイアウト)

「選ばれる3つの理由」のようなカード型セクションは、LPでは定番の構成要素です。auto-fitminmax()を組み合わせたCSS Gridパターンを使うと、メディアクエリなしでカードが画面幅に応じて自動的にリフローします。

.features {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
  gap: 2rem;
  padding: 4rem 1rem;
}

.feature-card {
  background: #fff;
  border-radius: 12px;
  padding: 2rem;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}

minmax(min(100%, 300px), 1fr)がこのパターンのコツです。カードの最小幅を300pxに設定しつつ、画面幅が300px未満のときは100%にフォールバックします。これにより、極端に狭い画面でもカードがはみ出すことがありません。

画面幅が900px以上あれば3列、600px程度なら2列、それ以下なら1列——と、ブレイクポイントを1行も書かずにレイアウトが自動調整されます。

Container Queriesでコンポーネント単位のレスポンシブを実現する

Container Queriesとは

メディアクエリはビューポート(ブラウザの画面幅)を基準にスタイルを切り替えますが、Container Queriesは親要素の幅を基準にします。これにより、コンポーネントが「自分が置かれた場所の幅」に応じてレイアウトを変えられるようになります。

MDN Web DocsのContainer Queriesガイドで詳しく解説されていますが、2024年時点で主要ブラウザすべてがサポートしており、本番環境で安心して使えます。

/* 親要素をコンテナとして定義 */
.pricing-wrapper {
  container-type: inline-size;
  container-name: pricing;
}

/* コンテナの幅に応じてスタイルを変更 */
@container pricing (min-width: 600px) {
  .pricing-cards {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 1.5rem;
  }
}

料金テーブルの実装例

LPの料金セクションは、プランの比較がしやすいレイアウトが求められます。Container Queriesを使うと、サイドバーに配置した場合でもメインカラムに配置した場合でも、同じHTMLが適切にレイアウトされます。

<div class="pricing-wrapper">
  <div class="pricing-cards">
    <div class="pricing-card">
      <h3>スタータープラン</h3>
      <p class="pricing-card__price">月額 <strong>3万円</strong></p>
      <ul class="pricing-card__features">
        <li>月4記事の作成</li>
        <li>キーワード分析レポート</li>
        <li>メールサポート</li>
      </ul>
      <a href="#contact" class="pricing-card__cta">このプランで相談</a>
    </div>
    <!-- 他のプランカードも同様 -->
  </div>
</div>
.pricing-wrapper {
  container-type: inline-size;
}

.pricing-cards {
  display: grid;
  gap: 1.5rem;
}

.pricing-card {
  border: 1px solid #e2e8f0;
  border-radius: 12px;
  padding: 2rem;
  text-align: center;
}

.pricing-card__price strong {
  font-size: 2rem;
  color: #2563eb;
}

/* コンテナ幅が500px以上で2列 */
@container (min-width: 500px) {
  .pricing-cards {
    grid-template-columns: repeat(2, 1fr);
  }
}

/* コンテナ幅が800px以上で3列 */
@container (min-width: 800px) {
  .pricing-cards {
    grid-template-columns: repeat(3, 1fr);
  }
  .pricing-card:nth-child(2) {
    transform: scale(1.05);
    border-color: #2563eb;
    box-shadow: 0 4px 12px rgba(37, 99, 235, 0.15);
  }
}

デスクトップでは推奨プランを少し大きく表示して視線を誘導し、モバイルでは等幅の縦積みにすることで操作しやすさを優先しています。

レスポンシブ画像の最適化実装

LPは画像が多用されるため、レスポンシブ画像の実装が表示速度に直結します。テックビルドのCore Web Vitals対応の記事でも解説していますが、ここでは実装パターンに焦点を当てます。

picture要素とsrcsetの使い分け

picture要素はアートディレクション(デバイスによって異なる画像を出し分ける)に、srcset属性は同じ画像の解像度違いを出し分けるのに使います。

<!-- アートディレクション: デバイスで画像自体を変える -->
<picture>
  <source
    srcset="/images/team-desktop.webp"
    media="(min-width: 768px)"
    type="image/webp"
    width="1200" height="600">
  <source
    srcset="/images/team-mobile.webp"
    type="image/webp"
    width="600" height="400">
  <img
    src="/images/team-mobile.jpg"
    alt="チームミーティングの様子"
    width="600" height="400"
    loading="lazy"
    decoding="async">
</picture>

<!-- 解像度の出し分け: 同じ構図で解像度だけ変える -->
<img
  srcset="/images/result-graph-400w.webp 400w,
          /images/result-graph-800w.webp 800w,
          /images/result-graph-1200w.webp 1200w"
  sizes="(min-width: 1024px) 50vw, 100vw"
  src="/images/result-graph-800w.jpg"
  alt="導入6ヶ月後のアクセス推移グラフ"
  width="800" height="450"
  loading="lazy"
  decoding="async">

ファーストビューに入る画像にはloading="eager"を、それ以降はloading="lazy"を指定します。widthheight属性を明示することで、画像読み込み前にブラウザがスペースを確保し、レイアウトシフト(CLS)を防げます。

aspect-ratioによるスペース確保

CSS のaspect-ratioプロパティを使うと、画像コンテナのサイズを事前に確保できます。

.lp-image-wrapper {
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-radius: 8px;
}

.lp-image-wrapper img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

CTAボタンのモバイル最適化

LPのコンバージョンに直結するCTAボタンは、特にモバイルでの操作性が重要です。デザイン面での考え方はUXDポータルのモバイルファーストLP設計の記事で詳しく解説されていますが、ここでは実装面のテクニックを紹介します。

タップ領域の確保

GoogleのWeb Vitalsガイドラインでは、タップターゲットの最小サイズとして48×48pxが推奨されています。LPのメインCTAは、これよりさらに大きく設計するのが実務的なベストプラクティスです。

.cta-button {
  display: block;
  width: 100%;
  max-width: 400px;
  margin: 0 auto;
  padding: 1rem 2rem;
  min-height: 56px;
  font-size: 1.125rem;
  font-weight: 700;
  text-align: center;
  text-decoration: none;
  color: #fff;
  background: #2563eb;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}

/* タッチデバイスではhoverエフェクトを無効化 */
@media (hover: hover) {
  .cta-button:hover {
    background: #1d4ed8;
    transform: translateY(-1px);
    box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
  }
}

/* タッチデバイスではactiveで反応 */
@media (hover: none) {
  .cta-button:active {
    background: #1d4ed8;
  }
}

@media (min-width: 768px) {
  .cta-button {
    display: inline-block;
    width: auto;
    padding: 1rem 3rem;
  }
}

touch-action: manipulationを指定すると、ダブルタップによるズームを防ぎ、タップの反応速度が向上します。また、@media (hover: hover)でhover対応デバイスとタッチデバイスを分けることで、スマホで「触れただけでhoverスタイルが残る」問題を回避できます。

固定CTAバーの実装

モバイルでは、画面下部に固定のCTAバーを表示するパターンが効果的です。ページをスクロールしても常にアクションボタンが見える状態を作ります。

.sticky-cta-bar {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  padding: 0.75rem 1rem;
  padding-bottom: calc(0.75rem + env(safe-area-inset-bottom));
  background: rgba(255, 255, 255, 0.95);
  backdrop-filter: blur(8px);
  border-top: 1px solid #e2e8f0;
  z-index: 100;
  transform: translateY(100%);
  transition: transform 0.3s ease;
}

.sticky-cta-bar.is-visible {
  transform: translateY(0);
}

/* デスクトップでは非表示 */
@media (min-width: 1024px) {
  .sticky-cta-bar {
    display: none;
  }
}

env(safe-area-inset-bottom)は、iPhoneのホームインジケーター(画面下部のバー)との重なりを防ぐための記述です。これを忘れると、iPhone Xシリーズ以降でCTAボタンがホームバーと重なってタップしにくくなります。

表示・非表示の切り替えはJavaScriptのIntersection Observerを使います。

const heroCTA = document.querySelector('.hero__cta');
const stickyCTA = document.querySelector('.sticky-cta-bar');

if (heroCTA && stickyCTA) {
  const observer = new IntersectionObserver(
    ([entry]) => {
      stickyCTA.classList.toggle('is-visible', !entry.isIntersecting);
    },
    { threshold: 0 }
  );
  observer.observe(heroCTA);
}

ヒーローセクション内のCTAボタンが画面外にスクロールされたタイミングで、固定CTAバーが表示される仕組みです。

実装時のチェックポイント

LPのレスポンシブ実装が完了したら、公開前に以下のポイントを確認してください。

レイアウト面の確認項目として、まずCSSのoverflow-x: hiddenに頼らず横スクロールが発生しないことを確認します。特にテーブルやコードブロックが原因になりがちです。次に、フォームの各入力欄がfont-size: 16px以上になっていることを確認してください。iOSのSafariでは16px未満の入力欄にフォーカスすると自動ズームが発生し、ユーザー体験を損ないます。この問題についてはテックビルドのフォーム最適化記事でも詳しく解説しています。

パフォーマンス面では、画像にwidthheight属性が設定されているか確認します。これが欠けているとCLS(Cumulative Layout Shift)が悪化し、ユーザーが読んでいる位置がずれるストレスにつながります。計測方法については、DTAポータルのGA4イベント設計の記事も参考になります。

アクセシビリティ面として、CTAボタンとリンクテキストが意味のある文言になっていることを確認してください。「こちら」ではなく「無料相談を予約する」のように、リンク先で何ができるかが文言だけで伝わるようにします。Web Content Accessibility Guidelines(WCAG)2.2では、リンクの目的がリンクテキスト単独で理解できることが求められています。

まとめ

LPのレスポンシブ実装は、モバイルファーストの設計方針を徹底することで、コードのシンプルさとユーザー体験の両方を手に入れられます。

この記事で紹介したテクニックの中から、まず取り組みやすいのは以下の3つです。

1つ目は、min-widthベースのメディアクエリに書き換えることです。既存のLPがmax-widthで書かれている場合でも、セクション単位で段階的に移行できます。

2つ目は、カード型レイアウトにauto-fitminmax()のGridパターンを導入することです。メディアクエリを削除できるケースも多く、コード量の削減にもつながります。

3つ目は、CTAボタンにtouch-action: manipulation@media (hover: hover)の分岐を追加することです。たった数行の追加で、タッチデバイスでの操作感が改善します。

LP改善をSEOの視点から進めたい場合は、SEOポータルのLP×オーガニック流入最適化の記事も合わせてご覧ください。技術的な実装とSEOの両面からLPを強化することで、広告費を抑えながらコンバージョンを伸ばすアプローチが見えてきます。

まずは来週、今あるLPのCTAボタンにtouch-action: manipulationを1行追加するところから始めてみてください。小さな変更ですが、スマホユーザーの反応速度が確実に変わります。