言語を切り替える
テーマを切り替える

shadcn/ui で管理画面の骨組みを構築:Sidebar + Layout ベストプラクティス

管理画面の拡張しやすい骨組みを、Sidebar コンポーネントから Next.js Layout の統合まで手を動かして構築します。完全なコードはそのまま使えます。


先週、管理画面システムの案件を引き受けました。真っ先に思い浮かんだのが shadcn/ui です。正直なところ、これまで Ant Design や MUI を使ってきて、スタイルのカスタマイズには毎回手こずる印象がありました。大量のスタイルを上書きするか、フレームワークのデザイン思想に縛られるか、どちらかになりがちだったのです。

shadcn/ui は違います。「Copy-paste」方式で、コードを直接プロジェクトに置くので、好きなように手を入れられます。2 週間使ってみて、これは確かに快適だと実感しました。特に Sidebar コンポーネントは、Next.js の App Router と組み合わせると、管理画面の骨組みがぐっとすっきり仕上がります。

この記事では、その実践プロセスを整理します。ゼロから始めて、拡張しやすい管理画面レイアウトを一緒に構築していきましょう。


一、なぜ shadcn/ui Sidebar を選ぶのか?

まずは私がハマってきた落とし穴から話します。

以前 Ant Design Pro を使ったときは、すぐ使えて確かに快適でした。ただ、プロジェクトが長く続くと頭が痛くなってきます。サイドバーのスタイルを変えたいだけでもドキュメントを延々と探す羽目になり、ちょっとした独自のインタラクションを入れようとすると、フレームワークの制約が多すぎることに気づきます。MUI にも似た問題がありました。テーマのカスタマイズは柔軟だと言われますが、それは Material Design を書けるという前提があってこそです。

従来手法の悩みどころ

Ant Design:機能は豊富ですが、カスタマイズのコストが高い。サイドバーの角丸を変えたいだけで、3 層分のスタイル上書きを書くことになったりします。

MUI:デザイン体系は完成されていますが、学習曲線が急。Styled Components の書き方は、チームの新人が慣れるのに 1 週間ほどかかります。

自前で実装:完全にコントロールできますが、レスポンシブ、アクセシビリティ、キーボード操作に対応したサイドバーをゼロから書くとなると、少なく見積もっても 3 日はかかります。

shadcn/ui の解き方

shadcn/ui は別の道を選んでいます:

  • Copy-paste 方式:コンポーネントのコードを直接プロジェクトに置くので、ブラックボックスな依存がありません
  • Radix UI ベース:アクセシビリティが組み込み済み。キーボード操作や ARIA 属性は最初から処理してくれます
  • Tailwind CSS 駆動:スタイルはクラス名そのもの。好きなように変えられ、スタイル上書きの煩わしさがありません

MUI から shadcn/ui へ移行したチームを何組も見てきました。理由はシンプルです。彼らが欲しいのは「コントロールできること」であって、すぐ使えるテンプレートではないのです。

向いている場面

次のようなものを作っているなら:

  • 中小規模の管理画面システム
  • SaaS プロダクトのコンソール
  • 社内ツールや運用プラットフォーム

shadcn/ui Sidebar は試す価値があります。完成された管理画面テンプレートをくれるわけではありませんが、十分に柔軟な骨組みを与えてくれます。


二、Sidebar コンポーネントの構成を理解する

手を動かす前に、shadcn/ui Sidebar のコンポーネント体系を理解しておきましょう。ここは公式ドキュメントでも比較的わかりやすく書かれている部分なので、ざっと一通り見ていきます。

主要コンポーネント一覧

Sidebar は一連のコンポーネントから成り、それぞれが役割を分担します:

SidebarProvider   // 状態のコンテキスト。アプリ全体をラップ
Sidebar          // サイドバーのコンテナ
SidebarHeader    // 上部の固定エリア。Logo を置く
SidebarContent   // スクロール可能なコンテンツ領域。メニューを置く
SidebarGroup     // メニューのグループ
SidebarMenu      // メニューのリスト
SidebarMenuItem  // メニュー項目
SidebarMenuButton // メニューボタン(Link 対応)
SidebarFooter    // 下部の固定エリア。ユーザー情報を置く
SidebarTrigger   // 折りたたみ/展開ボタン
SidebarInset     // メインコンテンツ領域のラッパー

コンポーネントは多く見えますが、関係はとても明快です:

SidebarProvider
├── Sidebar
│   ├── SidebarHeader
│   ├── SidebarContent
│   │   └── SidebarGroup
│   │       └── SidebarMenu
│   │           └── SidebarMenuItem
│   │               └── SidebarMenuButton
│   └── SidebarFooter
└── SidebarInset
    └── {children}

状態管理の仕組み

Sidebar の折りたたみ状態は SidebarProvider が管理します。モードは 2 つあります:

非制御モード(おすすめ):

<SidebarProvider defaultOpen={true}>
  <Sidebar />
</SidebarProvider>

制御モード

const [open, setOpen] = useState(true);

<SidebarProvider open={open} onOpenChange={setOpen}>
  <Sidebar />
</SidebarProvider>

ほとんどの場合は非制御モードで十分です。別の場所から Sidebar の状態をコントロールしたい(たとえばユーザー設定にトグルがあるなど)ときだけ、制御モードを使いましょう。

レスポンシブ設計の原理

Sidebar にはレスポンシブ対応が組み込まれています:

  • デスクトップ:サイドバーは左側に固定され、SidebarTrigger で折りたためます
  • モバイル:自動的にドロワー式(Sheet)になり、Trigger をタップすると表示されます

このロジックはコンポーネントの内部で処理されます。あなたは SidebarProvider で設定しておくだけで、あとはコンポーネントに任せられます。


三、Next.js Layout との統合実践

さて、コアとなる概念は説明し終わりました。次がいよいよ本題です。Sidebar を Next.js の Layout システムに統合していきます。

3.1 プロジェクト構成の設計

Next.js の Route Groups でレイアウトを整理するのがおすすめです。こうすると、機能ごとに異なるレイアウトを持たせつつ、URL 構造には影響を与えずに済みます。

app/
├── layout.tsx              # Root Layout(全体)
├── (marketing)/            # マーケティングページ群(Landing、About)
│   ├── layout.tsx          # Sidebar なし
│   └── page.tsx            # トップページ
├── (dashboard)/            # 管理画面ページ群
│   ├── layout.tsx          # Sidebar 付きのレイアウト
│   ├── page.tsx            # Dashboard ホーム
│   ├── users/
│   │   └── page.tsx        # ユーザー管理
│   └── settings/
│       └── page.tsx        # システム設定
└── (auth)/                 # 認証ページ群
    ├── layout.tsx          # 中央寄せレイアウト
    ├── login/
    │   └── page.tsx        # ログインページ
    └── register/
        └── page.tsx        # 登録ページ

この構成のよいところは次のとおりです:

  1. レイアウトの分離:マーケティングページには Sidebar が不要、管理画面ページには必要。Route Groups で自然に分けられます
  2. URL がシンプル(dashboard) は URL に現れないので、/users はそのまま /users です
  3. 拡張しやすい:ページ群を増やすときはフォルダを新しく作るだけです

3.2 Root Layout の設定

Root Layout はアプリ全体の入り口です。ここで全体に関わるもの、つまりテーマ、フォント、SidebarProvider を設定します。

// app/layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import { SidebarProvider } from "@/components/ui/sidebar";
import "./globals.css";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Admin Dashboard",
  description: "Built with shadcn/ui and Next.js",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    &lt;html lang="zh-CN"&gt;
      &lt;body className={inter.className}&gt;
        &lt;SidebarProvider&gt;
          {children}
        &lt;/SidebarProvider&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  );
}

ここで SidebarProvider を Dashboard Layout ではなく Root Layout に置いている点に注目してください。こうすると Sidebar の状態がページをまたいで保持されます(たとえば /users から /settings へ移動しても、折りたたみ状態が失われません)。

3.3 Dashboard Layout の実装

Dashboard Layout は管理画面の中心となるレイアウトで、Sidebar はここで導入します。

// app/(dashboard)/layout.tsx
import { AppSidebar } from "@/components/app-sidebar";
import { SidebarInset, SidebarTrigger } from "@/components/ui/sidebar";
import { Separator } from "@/components/ui/separator";
import {
  Breadcrumb,
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbList,
  BreadcrumbPage,
  BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    &lt;&gt;
      &lt;AppSidebar /&gt;
      &lt;SidebarInset&gt;
        &lt;header className="flex h-16 shrink-0 items-center gap-2 border-b px-4"&gt;
          &lt;SidebarTrigger className="-ml-1" /&gt;
          &lt;Separator orientation="vertical" className="mr-2 h-4" /&gt;
          &lt;Breadcrumb&gt;
            &lt;BreadcrumbList&gt;
              &lt;BreadcrumbItem className="hidden md:block"&gt;
                &lt;BreadcrumbLink href="/dashboard"&gt;
                  管理画面
                &lt;/BreadcrumbLink&gt;
              &lt;/BreadcrumbItem&gt;
              &lt;BreadcrumbSeparator className="hidden md:block" /&gt;
              &lt;BreadcrumbItem&gt;
                &lt;BreadcrumbPage&gt;概要&lt;/BreadcrumbPage&gt;
              &lt;/BreadcrumbItem&gt;
            &lt;/BreadcrumbList&gt;
          &lt;/Breadcrumb&gt;
        &lt;/header&gt;
        &lt;main className="flex-1 p-4 pt-6"&gt;{children}&lt;/main&gt;
      &lt;/SidebarInset&gt;
    &lt;/&gt;
  );
}

このレイアウトには次のものが含まれます:

  1. AppSidebar:自作のサイドバーコンポーネント(次の節で実装します)
  2. SidebarInset:メインコンテンツ領域のラッパー。Sidebar 折りたたみ時の幅を自動で処理します
  3. Header:上部のナビゲーションバー。SidebarTrigger とパンくずを含みます
  4. Main:メインコンテンツ領域

3.4 AppSidebar コンポーネントの実装

いよいよサイドバー本体を実装します。設定駆動の方式がおすすめです。ナビゲーションメニューを設定ファイルに置き、コンポーネントが設定に基づいてレンダリングします。

まずはナビゲーション設定を定義します:

// lib/navigation.ts
import {
  Home,
  Users,
  Settings,
  FileText,
  BarChart3,
  Shield,
} from "lucide-react";

export interface NavItem {
  title: string;
  href: string;
  icon: React.ComponentType&lt;{ className?: string }&gt;;
  badge?: string;
}

export const navConfig: NavItem[] = [
  {
    title: "概要",
    href: "/dashboard",
    icon: Home,
  },
  {
    title: "ユーザー管理",
    href: "/users",
    icon: Users,
    badge: "12", // バッジ
  },
  {
    title: "データ分析",
    href: "/analytics",
    icon: BarChart3,
  },
  {
    title: "コンテンツ管理",
    href: "/content",
    icon: FileText,
  },
  {
    title: "システム設定",
    href: "/settings",
    icon: Settings,
  },
  {
    title: "権限管理",
    href: "/permissions",
    icon: Shield,
  },
];

続いて AppSidebar を実装します:

// components/app-sidebar.tsx
"use client";

import Link from "next/link";
import { usePathname } from "next/navigation";
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
} from "@/components/ui/sidebar";
import { navConfig } from "@/lib/navigation";
import { Logo } from "@/components/logo";
import { UserNav } from "@/components/user-nav";

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

  return (
    &lt;Sidebar&gt;
      &lt;SidebarHeader className="border-b border-border"&gt;
        &lt;Logo /&gt;
      &lt;/SidebarHeader&gt;

      &lt;SidebarContent&gt;
        &lt;SidebarGroup&gt;
          &lt;SidebarGroupLabel&gt;ナビゲーションメニュー&lt;/SidebarGroupLabel&gt;
          &lt;SidebarGroupContent&gt;
            &lt;SidebarMenu&gt;
              {navConfig.map((item) =&gt; {
                const isActive = pathname === item.href;

                return (
                  &lt;SidebarMenuItem key={item.href}&gt;
                    &lt;SidebarMenuButton
                      asChild
                      isActive={isActive}
                      tooltip={item.title}
                    &gt;
                      &lt;Link href={item.href}&gt;
                        &lt;item.icon className="h-4 w-4" /&gt;
                        &lt;span&gt;{item.title}&lt;/span&gt;
                        {item.badge && (
                          &lt;span className="ml-auto text-xs bg-primary text-primary-foreground rounded-full px-2 py-0.5"&gt;
                            {item.badge}
                          &lt;/span&gt;
                        )}
                      &lt;/Link&gt;
                    &lt;/SidebarMenuButton&gt;
                  &lt;/SidebarMenuItem&gt;
                );
              })}
            &lt;/SidebarMenu&gt;
          &lt;/SidebarGroupContent&gt;
        &lt;/SidebarGroup&gt;
      &lt;/SidebarContent&gt;

      &lt;SidebarFooter className="border-t border-border"&gt;
        &lt;UserNav /&gt;
      &lt;/SidebarFooter&gt;
    &lt;/Sidebar&gt;
  );
}

ここで重要なのが ルートのハイライト です。usePathname() で現在のパスを取得し、item.href と比較します。一致したら SidebarMenuButtonisActive={true} を渡すと、コンポーネントが自動的にアクティブ状態のスタイルを適用してくれます。

3.5 多階層メニューの実装

管理画面に 2 階層目のメニューがある場合は、CollapsibleSidebarGroup をラップできます:

import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { ChevronDown } from "lucide-react";

// SidebarMenu の中で
&lt;Collapsible defaultOpen&gt;
  &lt;SidebarMenuItem&gt;
    &lt;CollapsibleTrigger asChild&gt;
      &lt;SidebarMenuButton&gt;
        &lt;Settings className="h-4 w-4" /&gt;
        &lt;span&gt;システム設定&lt;/span&gt;
        &lt;ChevronDown className="ml-auto h-4 w-4 transition-transform group-data-[state=open]/collapsible:rotate-180" /&gt;
      &lt;/SidebarMenuButton&gt;
    &lt;/CollapsibleTrigger&gt;
    &lt;CollapsibleContent&gt;
      &lt;SidebarMenuSub&gt;
        &lt;SidebarMenuSubItem&gt;
          &lt;SidebarMenuSubButton href="/settings/general"&gt;
            &lt;span&gt;基本設定&lt;/span&gt;
          &lt;/SidebarMenuSubButton&gt;
        &lt;/SidebarMenuSubItem&gt;
        &lt;SidebarMenuSubItem&gt;
          &lt;SidebarMenuSubButton href="/settings/security"&gt;
            &lt;span&gt;セキュリティ設定&lt;/span&gt;
          &lt;/SidebarMenuSubButton&gt;
        &lt;/SidebarMenuSubItem&gt;
      &lt;/SidebarMenuSub&gt;
    &lt;/CollapsibleContent&gt;
  &lt;/SidebarMenuItem&gt;
&lt;/Collapsible&gt;

四、応用機能の実装

基本のレイアウトができたので、次は実用的な応用機能を見ていきましょう。

4.1 権限制御(RBAC)

多くの管理画面では、ユーザーの役割に応じて表示するメニューを変える必要があります。実装はとてもシンプルです。ナビゲーション設定に roles フィールドを足し、レンダリング時にフィルタするだけです。

まずは設定を改修します:

// lib/navigation.ts
export interface NavItem {
  title: string;
  href: string;
  icon: React.ComponentType&lt;{ className?: string }&gt;;
  roles?: string[]; // アクセスを許可する役割
}

export const navConfig: NavItem[] = [
  {
    title: "概要",
    href: "/dashboard",
    icon: Home,
    // roles を設定しない=全員に表示
  },
  {
    title: "ユーザー管理",
    href: "/users",
    icon: Users,
    roles: ["admin", "manager"], // admin と manager だけに表示
  },
  {
    title: "権限管理",
    href: "/permissions",
    icon: Shield,
    roles: ["admin"], // admin だけに表示
  },
];

続いて AppSidebar 内で、ユーザーの役割に応じてフィルタします:

// components/app-sidebar.tsx
import { useAuth } from "@/hooks/use-auth"; // auth フックがある前提

export function AppSidebar() {
  const pathname = usePathname();
  const { user } = useAuth(); // 現在のユーザーを取得

  const filteredNav = navConfig.filter((item) =&gt; {
    if (!item.roles) return true; // 役割制限なし=全員に表示
    return item.roles.some((role) =&gt; user?.roles?.includes(role));
  });

  return (
    &lt;Sidebar&gt;
      {/* ... */}
      &lt;SidebarMenu&gt;
        {filteredNav.map((item) =&gt; {
          // ...
        })}
      &lt;/SidebarMenu&gt;
      {/* ... */}
    &lt;/Sidebar&gt;
  );
}

これで、一般ユーザーがログインしても「権限管理」のメニュー項目は見えなくなります。

4.2 外部リンクと区切り線

サイドバーに外部リンク(ドキュメントやヘルプセンターなど)を置きたいときや、区切り線でメニューをグループ分けしたいときもあります。shadcn/ui Sidebar はこれにも対応しています:

&lt;SidebarGroup&gt;
  &lt;SidebarGroupLabel&gt;主な機能&lt;/SidebarGroupLabel&gt;
  &lt;SidebarGroupContent&gt;
    &lt;SidebarMenu&gt;
      {/* 主なメニュー項目 */}
    &lt;/SidebarMenu&gt;
  &lt;/SidebarGroupContent&gt;
&lt;/SidebarGroup&gt;

&lt;SidebarGroup&gt;
  &lt;SidebarGroupLabel&gt;ヘルプとサポート&lt;/SidebarGroupLabel&gt;
  &lt;SidebarGroupContent&gt;
    &lt;SidebarMenu&gt;
      &lt;SidebarMenuItem&gt;
        &lt;SidebarMenuButton asChild&gt;
          &lt;a href="https://docs.example.com" target="_blank" rel="noopener"&gt;
            &lt;BookOpen className="h-4 w-4" /&gt;
            &lt;span&gt;ドキュメント&lt;/span&gt;
            &lt;ExternalLink className="ml-auto h-3 w-3" /&gt;
          &lt;/a&gt;
        &lt;/SidebarMenuButton&gt;
      &lt;/SidebarMenuItem&gt;
      &lt;SidebarMenuItem&gt;
        &lt;SidebarMenuButton asChild&gt;
          &lt;a href="mailto:support@example.com"&gt;
            &lt;HelpCircle className="h-4 w-4" /&gt;
            &lt;span&gt;お問い合わせ&lt;/span&gt;
          &lt;/a&gt;
        &lt;/SidebarMenuButton&gt;
      &lt;/SidebarMenuItem&gt;
    &lt;/SidebarMenu&gt;
  &lt;/SidebarGroupContent&gt;
&lt;/SidebarGroup&gt;

4.3 検索ボックスとクイック操作

多くの管理画面では、サイドバーに検索ボックスやグローバル検索(Cmd+K)を置きます。shadcn/ui には Command コンポーネントがあり、これを実現できます:

import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem } from "@/components/ui/command";

&lt;SidebarGroup&gt;
  &lt;SidebarGroupContent&gt;
    &lt;Command className="rounded-lg border shadow-md"&gt;
      &lt;CommandInput placeholder="メニューを検索..." /&gt;
      &lt;CommandList&gt;
        &lt;CommandEmpty&gt;結果が見つかりません&lt;/CommandEmpty&gt;
        &lt;CommandGroup heading="候補"&gt;
          {navConfig.map((item) =&gt; (
            &lt;CommandItem key={item.href} onSelect={() =&gt; router.push(item.href)}&gt;
              &lt;item.icon className="mr-2 h-4 w-4" /&gt;
              {item.title}
            &lt;/CommandItem&gt;
          ))}
        &lt;/CommandGroup&gt;
      &lt;/CommandList&gt;
    &lt;/Command&gt;
  &lt;/SidebarGroupContent&gt;
&lt;/SidebarGroup&gt;

五、パフォーマンス最適化とベストプラクティス

最後に、実践で役立つ最適化のポイントをいくつか取り上げます。

Server Components を優先する

Next.js App Router では、デフォルトですべてのコンポーネントが Server Component です。Sidebar の静的な部分(Logo や固定のメニュー項目など)は Server Component のままにしておき、インタラクションが必要な部分(ルートのハイライト、折りたたみ状態)だけ "use client" にできます。

私のやり方は次のとおりです:

  • AppSidebar"use client" を付ける(usePathname を使うため)
  • SidebarHeaderSidebarFooter 内の静的な部分は別の Server Component に切り出す
  • ナビゲーション設定はサーバー側で生成し、クライアントコンポーネントに渡す

こうするとクライアント側の JS サイズを減らせます。

大規模メニューの遅延読み込み

管理画面にメニュー項目が数十個あるなら、遅延読み込みを検討してもよいでしょう。React.lazy または Next.js の dynamic を使います:

import dynamic from "next/dynamic";

const AdminMenu = dynamic(() =&gt; import("./admin-menu"), {
  loading: () =&gt; &lt;SidebarMenuSkeleton /&gt;,
});

とはいえ正直なところ、ほとんどの管理画面はメニュー項目がそこまで多くならないので、この最適化が役立つ場面はあまりありません。

アクセシビリティのポイント

shadcn/ui の Sidebar は Radix UI ベースなので、アクセシビリティは基本的に組み込まれています。ただ、それでも気をつけたい点がいくつかあります:

  1. アイコン + 文字:アイコンだけにしないこと。スクリーンリーダーのユーザーには見えません
  2. フォーカスを見えるように:デフォルトのフォーカススタイルを上書きしないこと
  3. キーボード操作:Tab と方向キーがきちんと動くようにすること

Radix UI が大部分を処理してくれますが、コンポーネントを自分でカスタマイズしたときは、キーボード操作を必ずテストしておきましょう。


六、よくある質問

Q1: Sidebar の状態がリロード後に失われる?

SidebarProvider を Root Layout ではなく Dashboard Layout に置いていると、ルート切り替え時に状態がリセットされます。Provider を Root Layout に引き上げれば解決します。

Q2: モバイルで Sidebar を自動的に閉じるには?

shadcn/ui の Sidebar はモバイルで自動的に Sheet になります。メニュー項目をタップした後に、自分で閉じる処理を呼ぶ必要があります:

const { setOpenMobile } = useSidebar();

&lt;SidebarMenuButton
  onClick={() =&gt; setOpenMobile(false)}
&gt;

Q3: Sidebar の幅をカスタマイズするには?

CSS 変数を使います:

&lt;Sidebar
  style={{
    "--sidebar-width": "280px",
    "--sidebar-width-mobile": "100%",
  }}
&gt;

または sidebar.tsxSIDEBAR_WIDTH 定数を変更します。


まとめ

shadcn/ui Sidebar と Next.js Layout を組み合わせると、管理画面の骨組みを効率よく構築できます。要点を振り返りましょう:

  1. コンポーネント体系:SidebarProvider、Sidebar、SidebarContent などの役割を理解する
  2. Layout の統合:Route Groups でレイアウトを分離し、SidebarProvider は Root Layout に置く
  3. 設定駆動:ナビゲーションメニューを設定ファイルに置き、コンポーネントが設定に基づいてレンダリング。保守が楽になる
  4. ルートのハイライトusePathname() + isActive prop で、シンプルかつ直接的に
  5. 権限制御:設定に roles を足し、レンダリング時にフィルタする

この構成はいくつものプロジェクトで使ってきましたが、拡張性はなかなか優秀です。ページを追加するときは navConfig に 1 行足すだけで、あとはコンポーネントが処理してくれます。

質問があればコメント欄で気軽にどうぞ。次回は shadcn/ui DataTable の実践的な使い方を書く予定です。興味があればフォローしてみてください。


参考資料

shadcn/ui Sidebar + Next.js Layout で管理画面の骨組みを構築する

サイドバー、ルートのハイライト、権限制御を含む、拡張しやすい管理画面のレイアウトをゼロから構築します

⏱️ 目安時間: 45 分

  1. 1

    ステップ1: shadcn/ui を導入し Sidebar コンポーネントを追加する

    CLI コマンドを実行してプロジェクトを初期化し、コンポーネントを追加します:

    ```bash
    npx shadcn@latest init
    npx shadcn@latest add sidebar
    ```

    インストール中にスタイル設定を聞かれますが、デフォルトのままで構いません。完了すると components/ui ディレクトリに sidebar.tsx が生成されます。
  2. 2

    ステップ2: Root Layout を設定する

    app/layout.tsx で SidebarProvider をラップします:

    • SidebarProvider コンポーネントをインポート
    • body タグ内で {children} をラップ
    • lang="zh-CN" の言語属性を設定

    こうすると Sidebar の状態がアプリ全体で永続化されます。
  3. 3

    ステップ3: Dashboard Layout を作成する

    app/(dashboard)/layout.tsx に管理画面専用のレイアウトを作成します:

    • Route Groups 構文 (dashboard) を使用
    • AppSidebar と SidebarInset を導入
    • 上部の Header とパンくずナビゲーションを追加

    Route Groups は URL 構造に影響しないため、/dashboard のパスはそのままルートパスにマッピングされます。
  4. 4

    ステップ4: ナビゲーション設定を定義する

    lib/navigation.ts の設定ファイルを作成します:

    • NavItem インターフェースを定義(title、href、icon、badge)
    • navConfig 配列をエクスポート
    • 必要に応じて roles フィールドを追加し権限制御を実装

    設定駆動にすると、メニュー追加は 1 か所を直すだけで済みます。
  5. 5

    ステップ5: AppSidebar コンポーネントを実装する

    components/app-sidebar.tsx を作成します:

    • "use client" でクライアントコンポーネントとして指定
    • usePathname で現在のルートを取得
    • navConfig をループしてメニュー項目をレンダリング
    • ルートが一致したら isActive 属性を設定してハイライト
  6. 6

    ステップ6: 権限制御を追加する(任意)

    RBAC による権限フィルタを実装します:

    • NavItem インターフェースに roles フィールドを追加
    • AppSidebar 内で useAuth からユーザーの役割を取得
    • filter メソッドでメニュー項目を絞り込み

    roles フィールドがないメニューは、デフォルトで全ユーザーに表示されます。

FAQ

shadcn/ui Sidebar と Ant Design のサイドバーは何が違いますか?
shadcn/ui は Copy-paste 方式で、コンポーネントのコードを直接プロジェクトに置くため完全にコントロールできます。Ant Design は完成度の高いデザインシステムで、すぐ使える反面カスタマイズのコストは高めです。高度にカスタマイズしたいなら shadcn/ui、開発スピード重視なら Ant Design が向いています。
SidebarProvider は Root Layout と Dashboard Layout のどちらに置くべきですか?
Root Layout(app/layout.tsx)に置くのがおすすめです。こうすると Sidebar の折りたたみ状態がページをまたいで保持されます。Dashboard Layout に置くと、ルートを切り替えるたびに状態がリセットされてしまいます。
モバイルで Sidebar を自動的に閉じるにはどうすればいいですか?
モバイルでは Sidebar が自動的に Sheet(ドロワー)モードになります。メニュー項目をクリックしたときに次を呼び出してください:

```tsx
const { setOpenMobile } = useSidebar();
&lt;SidebarMenuButton onClick={() =&gt; setOpenMobile(false)}&gt;
```

こうするとメニューをタップした後にドロワーが自動的に閉じます。
Sidebar の幅をカスタマイズするには?
方法は 2 つあります:

1. CSS 変数を使う(おすすめ):
```tsx
&lt;Sidebar style={{ "--sidebar-width": "280px" }} /&gt;
```

2. sidebar.tsx の SIDEBAR_WIDTH 定数を変更する

CSS 変数のほうが柔軟で、Sidebar ごとに異なる幅を設定できます。
多階層メニューはどう実装しますか?
Collapsible コンポーネントで SidebarMenuItem をラップします:

• CollapsibleTrigger に 1 階層目のメニューボタンを入れる
• CollapsibleContent に SidebarMenuSub と 2 階層目のメニュー項目を入れる
• ChevronDown アイコンで開閉状態を示す

完全なコードは本文 3.5 節をご覧ください。
shadcn/ui Sidebar のアクセシビリティはどうですか?
Radix UI をベースにしているため、アクセシビリティのサポートが組み込まれています:キーボード操作(Tab/方向キー)、ARIA 属性、フォーカス管理など。あなたが気をつけるのは、アイコンと文字を必ず両方用意し、アイコンだけにしないことくらいです。

4分で読めます · 公開日: 2026年3月27日 · 更新日: 2026年6月8日

関連記事

コメント

GitHubアカウントでログインしてコメントできます