JavaScript

スマホでテーブルを横スクロールさせる方法|ヒント表示付き実装をデモで解説

みなと

テーブルは、情報を整理して見せるのにとても便利なレイアウトです。しかし、スマホで表示すると横幅に収まりきらず、PCではきれいに整っていたレイアウトも一気に読みにくくなってしまいます。では、スマホではテーブルをどのように見せるのが適切なのでしょうか?

対応方法はいくつかあります。レイアウトを組み替える方法もあれば、横スクロール前提で見せる方法もあります。この記事では、その中でも実務で採用されることの多い「横スクロール型」の実装方法を解説します。さらに、

  • 「横スクロールできます」という案内の表示
  • スクロール後に自動で非表示にする処理

まで含めた、実務でも使える構成をデモ付きで紹介します。スマホでのテーブル設計に悩んだことがある方は、ぜひ参考にしてみてください。

みなと
みなと

横スクロールのヒント表示は「ScrollHint」などのプラグインもありますが、今回はプラグインに頼らずに実装できる方法を紹介します。

スマホでのテーブル表示方法は大きく3つある

スマホでテーブルを表示する方法は、大きく分けて3つあります。

1)PCと同じレイアウトをそのまま縮小する

PC表示のテーブルをそのまま縮小してスマホ表示した例

もっともシンプルな方法です。特別な対応をせず、レスポンシブでそのまま表示します。

カラム数が少なく、情報量が限られているテーブルであれば、この方法でも十分に成立します。一方で、列が多い場合は文字が詰まりやすく、可読性が下がる可能性があります。

2)スマホ用にレイアウトを組み替える

テーブルをスマホ用に縦積みレイアウトへ組み替えて表示する例

テーブルの構造をそのまま使うのではなく、スマホ用にレイアウトを組み替える方法です。

よく使われるのは、th を横いっぱいに表示し、その下に対応する td を並べる“縦積み”のパターンです。横スクロールが不要になり、スマホでも読みやすくなります

ほかにも、1行ごとにカードのようなブロックに分解する方法もあります。可読性が高い一方で、レイアウト変更の影響が大きく、設計によってはワンソースでの管理が難しくなる場合もあります。

3)横スクロール前提で見せる

テーブルをPCと同じ構造のままスマホで横スクロール表示した例

PCと同じテーブル構造を維持し、スマホでは横スクロールできるようにする方法です。構造を大きく変更せずに対応できるのが特徴です。ただし、そのままだと「スクロールできることに気づきにくい」という課題があります。

そこで今回は、この「横スクロール型」をベースに、ヒント表示まで含めた実装方法を解説していきます。

完成デモの紹介

まずは、今回実装する完成デモをご覧ください。

スマホでテーブルを横スクロール表示する実装デモの画面

スマホ幅で表示すると、テーブルは横スクロールできる状態になります。さらに、初期表示では中央に「横スクロールできます」というヒントが表示されます。そして、少しでも横にスクロールすると、そのヒントは自動で非表示になります。

テーブル構造はPCと同じまま。レイアウトを大きく変更せずに、スマホでも扱いやすいUIにしています。

コード全体(HTML / CSS / JavaScript)

このセクションでは、今回のデモで使用しているコード全体を掲載します。まずはHTML・CSS・JavaScriptの構成を確認してみてください。詳細なポイントは、次のセクションで解説していきます。

HTMLコード

まずはHTMLコードです。テーブルをラップする要素の構造や、ヒント表示用のマークアップに注目してみてください。横スクロールを実現するために、テーブルの外側にコンテナ要素を用意しています。

<!-- 横スクロールの外枠:スマホ時にoverflow-x: autoを適用 -->
<div class="scroll-table">

  <!-- テーブルとヒントをまとめる内枠:スマホ時に固定幅を持たせる -->
  <div class="scroll-table-inner">

    <!-- テーブル本体 -->
    <div class="scroll-table-body">
      <table>
        <colgroup>
          <col style="width: 15%;">
          <col style="width: 50%;">
          <col style="width: 20%;">
          <col style="width: 15%;">
        </colgroup>
        <thead>
          <tr>
            <th>店舗名</th>
            <th>住所</th>
            <th>営業時間</th>
            <th>定休日</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td>渋谷店</td>
            <td>東京都渋谷区渋谷1-2-3 エムズビル2F</td>
            <td>10:00〜21:00</td>
            <td>火曜日</td>
          </tr>
          <tr>
            <td>新宿店</td>
            <td>東京都新宿区新宿4-5-6 サンライズビル1F</td>
            <td>11:00〜22:00</td>
            <td>水曜日</td>
          </tr>
          <!-- 以下、同様の構造が続きます -->
        </tbody>
      </table>
    </div>

    <!-- 横スクロールを促すヒント:JavaScriptでスクロール後に非表示になる -->
    <div class="scroll-hint">
      <div class="scroll-hint-icon"><img src="images/icon_scroll.webp" alt=""></div>
      <p class="scroll-hint-text">横スクロール<br>できます</p>
    </div>
  </div>
</div>

CSSコード

次にCSSコードです。スマホ幅でのみ横スクロールを有効にし、ヒントを中央に重ねて表示するスタイルを定義しています。.scroll-table.scroll-table-inner の役割に注目してみてください。

/* =====================
   横スクロール:外枠
   ===================== */

@media (max-width: 767px) {
  .scroll-table {
    position: relative; /* ヒントを中央に絶対配置するための基準 */
    margin: 0 -20px; /* 親要素のpaddingを打ち消してコンテンツ幅いっぱいに広げる */
    overflow-x: auto; /* スマホ時に横スクロールを有効にする */
  }
}

/* =====================
   横スクロール:内枠
   ===================== */

@media (max-width: 767px) {
  .scroll-table-inner {
    width: 1100px; /* スクロール領域の幅(コンテンツ幅に合わせて変更) */
    padding: 0 20px; /* 外枠で打ち消した分のpaddingを戻す */
  }
}

/* =====================
   テーブル本体
   ===================== */

.scroll-table-body table {
  width: 100%;
  table-layout: fixed; /* colgroup で指定した列幅を優先させる */
  border-spacing: 0;
  border-collapse: separate;
  background: #fff;
  font-size: 16px;
}

.scroll-table-body table th,
.scroll-table-body table td {
  padding: 15px 20px;
  line-height: 1.7;
  text-align: left;
}

.scroll-table-body table th:nth-child(n+2),
.scroll-table-body table td:nth-child(n+2) {
  border-left: 1px solid #e9edf2;
}

.scroll-table-body table th {
  background: #192753;
  color: #fff;
}

.scroll-table-body table tr:nth-child(n+2) th,
.scroll-table-body table tr:nth-child(n+2) td {
  border-top: 1px solid #e9edf2;
}

/* =====================
   横スクロールのヒント
   ===================== */

.scroll-hint {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 6px;
  position: absolute;
  left: 50%;
  top: 50%;
  width: 100px;
  height: 100px;
  box-sizing: border-box;
  border-radius: 5px;
  background: rgba(0, 0, 0, .8);
  transform: translate(-50%, -50%); /* 上下左右中央に配置 */
  transition: visibility 0ms 300ms, opacity 300ms; /* 非表示はopacityでフェードさせ、visibilityは遅延させる */
  pointer-events: none; /* ヒントがクリックの邪魔にならないようにする */
}

/* PC時はヒントを非表示 */
@media (min-width: 768px) {
  .scroll-hint {
    display: none;
  }
}

.scroll-hint-icon {
  position: relative;
  width: 48px;
  margin: 0 auto;
  aspect-ratio: 162/139;
}

.scroll-hint-icon img {
  display: block;
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.scroll-hint-text {
  color: #fff;
  font-size: 12px;
  font-weight: 700;
  line-height: 1.3;
  text-align: center;
}

/* スクロール後にヒントをフェードアウトして非表示にする */
.scroll-table.is-scrolled .scroll-hint {
  visibility: hidden;
  opacity: 0;
}

JavaScriptコード

最後にJavaScriptコードです。スクロール状態を判定し、ヒントを非表示にする処理を実装しています。リサイズ時の再判定にも対応しています。

const scroll_table = (() => {
  // 各種設定値をまとめるオブジェクト
  const props = {
    resizeDelay: 300, // リサイズ後に再判定するまでの待機時間(ms)
    resizeTimeout: null // setTimeout を保持しておくための変数
  };

  function init() {
    // ページ内の .scroll-table をすべて取得(複数設置にも対応)
    const scrollTables = document.querySelectorAll('.scroll-table');
    if (!scrollTables.length) return; // 該当要素がなければ処理しない

    // 初期表示時にスクロール状態を判定
    scrollTables.forEach((el) => {
      scrollCheck(el);
    });

    // 画面リサイズ時にも再判定(向き変更などに対応)
    window.addEventListener('resize', () => {
      clearTimeout(props.resizeTimeout);

      // 連続発火を防ぐためにディレイをかける(簡易デバウンス)
      props.resizeTimeout = setTimeout(() => {
        scrollTables.forEach((el) => {
          scrollCheck(el);
        });
      }, props.resizeDelay);
    });

    // 実際に横スクロールされたときの判定
    scrollTables.forEach((el) => {
      el.addEventListener('scroll', (e) => {
        scrollCheck(e.currentTarget);
      });
    });
  }
  
  /**
   * スクロール状態を判定し、
   * ヒントを非表示にするためのクラス(is-scrolled)を付与する
   */
  function scrollCheck(scroll) {
    // display:none などで非表示の場合は処理しない
    // PC幅ではヒント自体が非表示なので判定不要
    const isHidden = scroll.offsetParent === null;
    if (isHidden || window.matchMedia('(min-width: 768px)').matches) return;

    let scrolledFlag = false;

    // 外枠(スクロール領域)の幅
    const scrollW = scroll.getBoundingClientRect().width;

    // 内側コンテンツの幅(固定幅)
    const scrollInner = scroll.querySelector('.scroll-table-inner');
    const scrollInW = scrollInner.getBoundingClientRect().width;

    // ① そもそも横スクロールが不要な場合(内側が収まっている)
    if (scrollW >= scrollInW) { scrolledFlag = true; }

    // 現在のスクロール位置
    const scrL = scroll.scrollLeft;

    // ② 少しでも右にスクロールされたらヒントを消す
    if (scrL > 10) { scrolledFlag = true; }

    // ③ 一番右端までスクロール済みの場合もヒント不要
    if (scrL >= scrollInW - scrollW) { scrolledFlag = true; }

    // 条件を満たしたらクラスを付与(CSSでフェードアウト)
    if (scrolledFlag) {
      scroll.classList.add('is-scrolled');
    }
  }
  
  return {
    init: init
  };
})();

// 初期化実行
scroll_table.init();

コードのポイント解説

ここからは、今回の実装で押さえておきたいポイントを解説します。

ポイント1:外枠と内枠の役割を分ける

横スクロールを実装するポイントは、「スクロールさせる要素」と「横幅を持たせる要素」を分けることです。

横スクロール実装における外枠(.scroll-table)と内枠(.scroll-table-inner)の役割を示した図

.scroll-table は、横スクロールを発生させるための「外枠」です。スマホ幅のときだけ overflow-x: auto; を指定し、スクロール可能な領域にしています。

一方、.scroll-table-inner はテーブル全体を包む「内枠」です。ここに固定幅を持たせることで、スマホ画面よりも横に広い状態を作り、スクロールを発生させています。

  • 外枠 → 可変(画面幅に合わせる)
  • 内枠 → 固定(PC幅を維持する)

この役割分担が基本設計です。

ポイント2:状態をクラスで管理する設計

今回の実装では、スクロール後にヒントを消すための状態を .is-scrolled というクラスで管理しています。JavaScript側は「スクロールされたかどうか」を判定してクラスを付けるだけ。ヒントをフェードアウトして非表示にする見た目の制御は、CSSに任せています。

みなと
みなと

状態の管理はJS、見た目の変化はCSSという役割分担にしておくと、コードが読みやすくなり、あとから調整もしやすくなります。

ポイント3:JavaScriptの全体像をつかむ

このJavaScriptは、大きく分けて次の3つを行っています。

  1. .scroll-table を取得して初期状態を判定する
  2. スクロールされたら再判定して、必要なら .is-scrolled を付与する
  3. 画面リサイズ(向き変更など)でも再判定する

つまり、「いまヒントを表示すべきか?」を状況に応じてチェックし、条件を満たしたらクラスを付けるだけのシンプルな構造です。

初期表示・スクロール・画面リサイズ時に scrollCheck() を実行し、条件を満たすと .is-scrolled を付与してヒントを非表示にする処理フロー図

このスクリプトでは、スクロール時だけでなく「初期表示」と「画面リサイズ」のタイミングでも状態を判定しています。理由は、画面幅によってはテーブルが収まり、横スクロールが不要になる場合があるためです。また、スマホの縦横回転などで画面幅が変わると、スクロールの必要性が変わることもあります。

そのため、表示状況に合わせてヒント表示を正しく制御できるよう、これらのタイミングでも再判定を行っています。

みなと
みなと

ちなみに今回のデモでは、内側の幅を1100pxに固定しているため、スマホでは必ず横スクロールが発生します。ただ、より汎用的に使えるよう、スクロールが不要なケースも想定してこの判定を入れています。

ポイント4:スクロール判定のロジック

ヒントを非表示にするかどうかは、scrollCheck() の中で判定しています。この処理では、次の3つの状態をチェックしています。

  • そもそも横スクロールが不要な場合
  • 少しでも横にスクロールされた場合
  • 右端までスクロールされた場合
if (scrollW >= scrollInW) { scrolledFlag = true; }

const scrL = scroll.scrollLeft;
if (scrL > 10) { scrolledFlag = true; }

if (scrL >= scrollInW - scrollW) { scrolledFlag = true; }

まず、外枠と内枠の幅を比較し、テーブルが画面内に収まっている場合はヒントを表示する必要がないため、すぐに非表示にします。次に、scrollLeft を使ってスクロール位置を取得し、少しでも横に動いたらヒントを消すようにしています。さらに、右端までスクロールされている場合も、ヒントは不要になるため非表示にしています。

このように、複数の状態を考慮してヒントの表示を制御しているのがポイントです。

横スクロール不要・少しスクロール・右端までスクロールのいずれかでヒントを非表示にする判定条件の図

まとめ

スマホでテーブルを表示する方法はいくつかありますが、横スクロール前提のレイアウトも有効な選択肢の一つです。今回の実装では、テーブル構造を維持したまま横スクロールを可能にし、ヒント表示によってスクロールできることを分かりやすくしています。

特別なライブラリを使わなくても、HTML / CSS / JavaScriptだけでシンプルに実装できます。スマホでのテーブル表示に悩んだときは、ぜひ参考にしてみてください。

押していただけると励みになります!

ABOUT ME
みなと
みなと
フロントエンドエンジニア
東京のWeb制作会社で15年以上働いている現役フロントエンドエンジニアです。これまで、いろんなプロジェクトに関わりながら、フロントエンド開発やWebデザインに取り組んできました。このブログでは、今までの経験を活かして、Web制作に役立つ情報やノウハウをシェアしていきたいと思います。初心者の方から、現場で働く方まで、誰でも参考にできる内容をお届けしますので、ぜひ覗いてみてください。
記事URLをコピーしました