【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 のパスを配列に入れておき、ラベルと一緒に縦に並べます。
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" を付けます。
"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 で画面の端まで使う設定を入れ、タブバーの下にホームバーの高さ分の余白を足します。
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 は、同じ名前の要素が別の場所に現れたとき、前の位置から動いて見せる仕組みです。
"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 の「センタープラス・タブバー」は、画面に浮かぶ白い 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 で解説しています。