SystemSidebar(左サイドバーナビ)
:LiTarget: 用途
左固定の240px幅サイドバー。グループ別にリンクを並べて、現在地をハイライト。
- グループタイトル(小文字大文字)
- 各リンクにアイコン + テキスト
- 現在のパスをハイライト表示
- aliasパスにも反応(href が
/fooでも、/foo/barで active 判定) - mobile(md未満)は非表示(モバイルは別途ハンバーガー実装)
:LiCode: コード(コピペ用・骨格)
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { LayoutDashboard, Settings /* etc */ } from 'lucide-react';
import type { ComponentType, SVGProps } from 'react';
type IconType = ComponentType<SVGProps<SVGSVGElement> & { size?: number | string }>;
type Item = { label: string; href: string; icon: IconType; aliases?: string[] };
type Group = { name: string; items: Item[] };
// ─── ここをカスタマイズ ───
const GROUPS: Group[] = [
{
name: '全体',
items: [
{ label: 'ダッシュボード', href: '/', icon: LayoutDashboard },
],
},
{
name: '設定',
items: [
{ label: 'アカウント', href: '/account', icon: Settings },
],
},
];
function normalize(p: string) {
if (!p) return '/';
const trimmed = p.replace(/\/+$/, '');
return trimmed === '' ? '/' : trimmed;
}
export default function SystemSidebar() {
const pathname = usePathname() || '/';
const current = normalize(pathname);
return (
<aside
className="hidden md:flex md:flex-col w-[240px] shrink-0 bg-bg2 border-r border-border overflow-y-auto"
style={{ height: 'calc(100vh - 44px)', position: 'sticky', top: 44 }}
>
<nav className="p-3">
{GROUPS.map((group) => (
<div key={group.name} className="mb-4">
<div className="text-[10px] font-semibold text-text3 uppercase tracking-wider px-2 mb-1">
{group.name}
</div>
{group.items.map((item) => {
const itemHref = normalize(item.href);
const aliasMatched = item.aliases?.some((a) =>
current === normalize(a) || current.startsWith(normalize(a) + '/')
);
const active =
current === itemHref ||
(itemHref !== '/' && current.startsWith(itemHref + '/')) ||
aliasMatched;
const Icon = item.icon;
return (
<Link key={item.href} href={item.href}
className={`flex items-center gap-2 px-2 py-1.5 rounded text-xs transition-colors ${
active ? 'bg-bg3 text-text font-medium' : 'text-text2 hover:bg-bg3 hover:text-text'
}`}>
<Icon size={14} />
<span className="truncate">{item.label}</span>
</Link>
);
})}
</div>
))}
</nav>
</aside>
);
}
:LiHandPointer: 使用例(layout.tsx に組み込み)
import SystemSidebar from '@/components/layout/SystemSidebar';
export default function RootLayout({ children }) {
return (
<html lang="ja">
<body className="min-h-screen bg-bg text-text">
<header className="sticky top-0 z-30 h-11 bg-bg2 border-b border-border px-4 flex items-center justify-between">
{/* ヘッダー内容 */}
</header>
<div className="flex">
<SystemSidebar />
<main className="flex-1 min-w-0 p-4 md:p-6">{children}</main>
</div>
</body>
</html>
);
}
:LiAlertCircle: 注意事項
- header が固定 44px 前提(変える場合は SystemSidebar の
top: 44も合わせる) 'use client'必須(usePathname のため)- Next.js 16 App Router 前提(pages router では別途実装が必要)
- aliases に複数パスを入れると、サブパス遷移時もハイライト維持できる
:LiLightbulb: 拡張アイデア
- グループの折りたたみ(useState で open/close)
- 検索ボックス(filter で items 絞り込み)
- お気に入りピン留め
- mobile ドロワー(md未満で表示・ハンバーガー)