【Next.js】スマホ向けボトムタブバーの作り方を現在地の表示まで解説

公開日: 2026年10月8日 · 読了目安 8 分

Next.js のサイトに、スマホ向けのボトムタブバー(画面下のナビ)を置く手順です。画面下への固定、iPhone のホームバーとの重なり対策、usePathname で今いるタブを示す方法、Motion で動くインジケーター、本文が隠れないための余白まで、コード付きで解説します。

スマホのナビをボトムタブバーにして親指で届くようにする

Next.js でアプリのようなサイトを作っていて、スマホではハンバーガーメニューにした。でも画面右上まで指を伸ばすのが面倒で、ページを行き来しにくい。

そんなときに合うのが、画面の下に固定するボトムタブバーです。ボトムナビゲーションとも呼ばれます。

作り方の答えは次のとおりです。リンクを並べた nav を画面下に固定し、iPhone のホームバーの分だけ下に余白を足します。

今いるページは、URL を読む usePathname で判定します。この部分だけ、ブラウザで動かす印の "use client" を付けます。

この記事で分かることは次のとおりです。

  • ボトムタブバーが向くサイトと、向かないサイト

  • 画面下に固定したタブバーの基本の形

  • 今いるタブを色と aria-current で示す方法

  • iPhone のホームバーと重ならない余白の取り方

  • Motion で下線がタブ間を滑るインジケーター

ボトムタブバーが向くサイトと向かないサイト

ボトムタブバーは、スマホで何度もページを行き来するサイトに向いています。理由は3つあります。

1つ目は、親指が届きやすいことです。片手で持つと、画面の下側がいちばん押しやすい場所になります。

2つ目は、行き先が常に見えていることです。メニューを開く手間がなく、今どこにいるかも一目で分かります。

3つ目は、アプリと同じ操作になることです。多くの人がスマホアプリで慣れている形なので、説明が要りません。

向いているのは、行き先が3〜5個に絞れるサイトです。例えば次のような場合です。

  • 検索・お気に入り・マイページを行き来するサービス

  • ログイン後のダッシュボードやマイページ

  • ホーム画面に追加して使う PWA(アプリのように使える Web サイト)

反対に、1ページで完結する LP や、メニューが6個以上ある企業サイトには向きません。その場合はヘッダーと開閉するメニューの方が扱いやすいです。

開閉メニューの作り方は https://design-layer.com/ja/articles/nextjs-mobile-nav-animation-guide で解説しています。

Step1:画面下に固定したボトムタブバーを作る

まずは、リンクを横に並べたボトムタブバーを画面下に固定します。この段階では状態を持たないので、サーバーで組み立てる部品のまま置けます。

アイコンは SVG のパスを配列に入れておき、ラベルと一緒に縦に並べます。

components/BottomTabBar.tsx
import Link from "next/link";

export const tabs = [
  { href: "/", label: "ホーム", icon: "M3 10.5 12 3l9 7.5V21h-6v-6H9v6H3z" },
  { href: "/search", label: "さがす", icon: "M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14zm9 16-4.35-4.35" },
  { href: "/favorites", label: "お気に入り", icon: "M12 20s-7-4.35-7-10a4 4 0 0 1 7-2.65A4 4 0 0 1 19 10c0 5.65-7 10-7 10z" },
  { href: "/account", label: "マイページ", icon: "M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zm-7 9a7 7 0 0 1 14 0" },
];

export function TabIcon({ d }: { d: string }) {
  return (
    <svg
      aria-hidden="true"
      viewBox="0 0 24 24"
      className="h-6 w-6"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.8}
      strokeLinecap="round"
      strokeLinejoin="round"
    >
      <path d={d} />
    </svg>
  );
}

export function BottomTabBar() {
  return (
    <nav
      aria-label="メインメニュー"
      className="fixed inset-x-0 bottom-0 z-40 border-t border-zinc-200 bg-white/95 pb-[env(safe-area-inset-bottom)] backdrop-blur md:hidden"
    >
      <ul className="mx-auto flex max-w-md">
        {tabs.map((tab) => (
          <li key={tab.href} className="flex-1">
            <Link
              href={tab.href}
              className="flex min-h-14 flex-col items-center justify-center gap-0.5 text-[11px] text-zinc-500"
            >
              <TabIcon d={tab.icon} />
              <span>{tab.label}</span>
            </Link>
          </li>
        ))}
      </ul>
    </nav>
  );
}

コードのポイント

  • fixed と bottom-0 で、スクロールしても画面下に残るようにする

  • md:hidden で、タブレット以上の幅では隠す

  • 各タブを flex-1 にして、横幅を均等に分ける

  • pb-[env(safe-area-inset-bottom)] は iPhone のホームバー対策。詳しくは Step3 で説明する

アイコンとラベルを両方出す

アイコンだけのタブは見た目がすっきりします。ただ、初めて来た人には意味が伝わりにくくなります。

ハートが「お気に入り」なのか「いいね」なのかは、ラベルがあれば迷いません。ラベルは2〜5文字ほどに収めます。

アイコンには aria-hidden を付け、読み上げはラベルの文字に任せます。

タップ領域は 44px 以上にする

指で押す部品は、高さと幅を 44px 以上にするのが目安です。上のコードでは min-h-14 で高さを 56px にしています。

タブを5個にすると、幅の狭いスマホでも1つ 70px 前後になります。6個以上にすると押し間違いが増えるので、5個までにします。

Step2:ボトムタブバーで今いるタブを示す

タブバーには「今どこにいるか」を示す役目もあります。Next.js では、usePathname で今の URL のパスを読めます。

usePathname はブラウザで動く部品でしか使えません。そのため、このファイルの先頭に "use client" を付けます。

components/BottomTabBar.tsx(BottomTabBar を差し替え)
"use client";

import Link from "next/link";
import { usePathname } from "next/navigation";

// tabs と TabIcon は Step1 と同じ

function isActive(pathname: string, href: string) {
  if (href === "/") return pathname === "/";
  return pathname === href || pathname.startsWith(href + "/");
}

export function BottomTabBar() {
  const pathname = usePathname();

  return (
    <nav
      aria-label="メインメニュー"
      className="fixed inset-x-0 bottom-0 z-40 border-t border-zinc-200 bg-white/95 pb-[env(safe-area-inset-bottom)] backdrop-blur md:hidden"
    >
      <ul className="mx-auto flex max-w-md">
        {tabs.map((tab) => {
          const active = isActive(pathname, tab.href);
          return (
            <li key={tab.href} className="flex-1">
              <Link
                href={tab.href}
                aria-current={active ? "page" : undefined}
                className={
                  "flex min-h-14 flex-col items-center justify-center gap-0.5 text-[11px] transition-colors " +
                  (active ? "font-semibold text-zinc-900" : "text-zinc-500")
                }
              >
                <TabIcon d={tab.icon} />
                <span>{tab.label}</span>
              </Link>
            </li>
          );
        })}
      </ul>
    </nav>
  );
}

下の階層のページでもタブを光らせる

パスが完全に一致したときだけ光らせると、/account/settings を開いたときにマイページのタブが消えてしまいます。

そこで isActive では、/account で始まるパスも「マイページにいる」と判定しています。

ホームの / だけは別扱いです。すべてのパスが / で始まるため、完全一致のときだけ光らせます。

aria-current="page" で現在地を読み上げる

色を変えるだけでは、画面を見ない人には現在地が伝わりません。今いるタブのリンクには aria-current="page" を付けます。

スクリーンリーダーは、このリンクを「現在のページ」と読み上げます。色に加えて太字にしておくと、色が見分けにくい人にも伝わります。

Step3:ボトムタブバーを iPhone のホームバーと重ねない

ホームボタンのない iPhone では、画面の一番下に横長のホームバーがあります。何もしないと、タブバーとホームバーが近すぎて押しにくくなります。

対策は2つです。layout.tsx で画面の端まで使う設定を入れ、タブバーの下にホームバーの高さ分の余白を足します。

app/layout.tsx
import type { Viewport } from "next";
import { BottomTabBar } from "@/components/BottomTabBar";
import "./globals.css";

export const viewport: Viewport = {
  width: "device-width",
  initialScale: 1,
  viewportFit: "cover",
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ja">
      <body>
        <main className="pb-[calc(4rem_+_env(safe-area-inset-bottom))] md:pb-0">
          {children}
        </main>
        <BottomTabBar />
      </body>
    </html>
  );
}

viewport で viewportFit: "cover" を指定する

Next.js 15 では、viewport の設定を layout.tsx の viewport という書き出しで指定します。viewportFit: "cover" は、画面の端まで使うという指定です。

この指定がないと、env(safe-area-inset-bottom) の値が 0 のままになる場合があります。env() は、端末が知らせる余白の値を CSS で読む関数です。

Step1 のタブバーでは、この値をそのまま下の余白にしています。ホームバーのない端末では 0 になるので、余計な隙間はできません。

本文の最後がボトムタブバーに隠れないようにする

画面下に固定した部品は、本文の上に重なります。そのままだと、ページの一番下のボタンや文章がタブバーの裏に隠れます。

そこで main の下に、タブバーの高さとホームバーの余白を足した分の余白を入れます。タブバーを隠す md 以上の幅では、余白を 0 に戻します。

タブバーを layout.tsx に置くと、ページを移動してもタブバーは作り直されません。Step4 のアニメーションもこの置き方が前提です。

Step4:ボトムタブバーに滑るインジケーターを付ける

今いるタブの上に細い線を出し、タブを押すとその線が横に滑るようにします。Motion(旧 Framer Motion)の layoutId を使うと、数行で作れます。

layoutId は、同じ名前の要素が別の場所に現れたとき、前の位置から動いて見せる仕組みです。

components/BottomTabBar.tsx(BottomTabBar を差し替え)
"use client";

import Link from "next/link";
import { usePathname } from "next/navigation";
import { motion, useReducedMotion } from "motion/react";

// tabs・TabIcon・isActive は Step1・Step2 と同じ

export function BottomTabBar() {
  const pathname = usePathname();
  const reduceMotion = useReducedMotion();

  return (
    <nav
      aria-label="メインメニュー"
      className="fixed inset-x-0 bottom-0 z-40 border-t border-zinc-200 bg-white/95 pb-[env(safe-area-inset-bottom)] backdrop-blur md:hidden"
    >
      <ul className="mx-auto flex max-w-md">
        {tabs.map((tab) => {
          const active = isActive(pathname, tab.href);
          return (
            <li key={tab.href} className="flex-1">
              <Link
                href={tab.href}
                aria-current={active ? "page" : undefined}
                className={
                  "relative flex min-h-14 flex-col items-center justify-center gap-0.5 text-[11px] transition-colors " +
                  (active ? "font-semibold text-zinc-900" : "text-zinc-500")
                }
              >
                {active && (
                  <motion.span
                    layoutId="tab-indicator"
                    aria-hidden="true"
                    className="absolute inset-x-5 top-0 h-0.5 rounded-full bg-zinc-900"
                    transition={
                      reduceMotion
                        ? { duration: 0 }
                        : { type: "spring", stiffness: 500, damping: 40 }
                    }
                  />
                )}
                <TabIcon d={tab.icon} />
                <span>{tab.label}</span>
              </Link>
            </li>
          );
        })}
      </ul>
    </nav>
  );
}

コードのポイント

  • 線は今いるタブの中にだけ描く。タブが変わると、Motion が前のタブの位置から滑らせる

  • Link に relative を付け、線をタブの上端に置く

  • 動きを減らす設定の人には、useReducedMotion で移動を一瞬にする

  • 線は飾りなので aria-hidden を付ける。現在地は aria-current が伝える

Motion の基本は https://design-layer.com/ja/articles/nextjs-framer-motion-guide で解説しています。

ボトムタブバーでよくある失敗と公開前チェック

  • タブを6個以上並べる。押し間違いが増えるので、5個までに絞る

  • アイコンだけでラベルがない。初めての人が迷う

  • 本文の下に余白がなく、最後のボタンがタブバーに隠れる

  • viewportFit を指定せず、iPhone でホームバーと重なる

  • PC でもタブバーを出す。広い画面ではヘッダーのナビにする

  • 今いるタブが色だけで、aria-current が付いていない

  • page.tsx 全体に "use client" を書く。タブバーのファイルだけに付ける

画面下に固定したチャットボタンやクッキーの案内がある場合は、タブバーと重ならないかも確かめます。それらの bottom の位置を、タブバーの高さ分だけ上げてください。

PC 向けのヘッダーの作り方は https://design-layer.com/ja/articles/nextjs-modern-header-guide で解説しています。

完成したボトムタブバーをコピーして使う方法

DesignLayer の「センタープラス・タブバー」の詳細ページ。左でライブプレビューを確かめ、右の「Cursor 用にコピー」でソースをコピーできる
DesignLayer の「センタープラス・タブバー」の詳細ページ。左でライブプレビューを確かめ、右の「Cursor 用にコピー」でソースをコピーできる

下線のインジケーターより凝った見た目にしたい場合は、完成したタブバーをブラウザで動かして確かめる方法もあります。

DesignLayer の「センタープラス・タブバー」は、画面に浮かぶ白い pill 型のタブバーです。ホーム・検索・メッセージ・通知・プロフィールを並べます。

選んだタブの位置に円がスライドし、バーを半円にえぐる動きが付いています。

カタログでは、ライブプレビューで動きを見てから「Cursor 用にコピー」を押します。統合の指示・ライブプレビューの URL・本体の TSX・npm 依存がまとめてコピーされ、Cursor や Claude に貼って使えます。

閲覧とライブプレビューは無料で、アカウントもカードも要りません。無料のソースコピーは日本時間で1日3回です。

カタログは https://design-layer.com/ja/components から開けます。載せたあとは、リンク先・ラベル・アクセント色を自分のサイトに合わせて差し替えます。

ボトムタブバーについてよくある質問

  • 行き先が3〜5個で、何度も行き来するならボトムタブバーが向いています。押すまでの手間が1回少なく、今いる場所も常に見えます。

    メニューが6個以上ある場合や、LP のように上から順に読むページなら、ハンバーガーメニューの方が収まりがいいです。

まとめ:Next.js でボトムタブバーを作る手順

  • ボトムタブバーは、行き先が3〜5個で何度も行き来するサイトに向く

  • nav を画面下に固定し、アイコンとラベルを並べる。タップ領域は 44px 以上にする

  • usePathname で今いるタブを判定し、aria-current="page" を付ける

  • viewportFit: "cover" と env(safe-area-inset-bottom) で、iPhone のホームバーと重ねない

  • 本文の下にタブバーの高さ分の余白を入れ、PC では隠す

  • 滑るインジケーターは Motion の layoutId で作る

まずは Step1 のコードに、自分のサイトの行き先を4つ入れてみてください。スマホのメニュー全体の作り方は https://design-layer.com/ja/articles/nextjs-mobile-nav-animation-guide で解説しています。

【Next.js】スマホ向けボトムタブバーの作り方を現在地の表示まで解説 | DesignLayer