T-CREATOR

shadcn/ui でダッシュボードをデザインするベストプラクティス

shadcn/ui でダッシュボードをデザインするベストプラクティス

モダンな Web アプリケーション開発において、効率的で美しいダッシュボードの構築は重要な課題です。shadcn/ui を活用することで、この課題を優雅に解決できるでしょう。

本記事では、shadcn/ui を使用したダッシュボード開発のベストプラクティスをご紹介します。初心者の方でも段階的に理解できるよう、基礎から実践的な実装まで詳しく解説いたします。

背景

shadcn/ui の特徴と利点

shadcn/ui は、Radix UI と Tailwind CSS を基盤とした革新的な UI コンポーネントライブラリです。従来のライブラリとは異なり、コンポーネントをコピー&ペーストで利用できる独特なアプローチを採用しています。

以下の図は shadcn/ui の基本構造を示しています。

mermaidflowchart TB
    shadcn[shadcn/ui] --> radix[Radix UI<br/>アクセシビリティ基盤]
    shadcn --> tailwind[Tailwind CSS<br/>スタイリング]
    shadcn --> typescript[TypeScript<br/>型安全性]

    radix --> components[コンポーネント群]
    tailwind --> components
    typescript --> components

    components --> dashboard[ダッシュボード<br/>アプリケーション]

shadcn/ui の主な利点は以下の通りです。

項目特徴メリット
1アクセシビリティ対応Radix UI ベースで WAI-ARIA 準拠
2カスタマイズ性ソースコードを直接編集可能
3型安全性TypeScript で完全に記述
4軽量性必要なコンポーネントのみ導入
5デザインシステム一貫したデザイントークン

ダッシュボード開発における課題

現代のダッシュボード開発では、複数の技術的課題に直面します。特に以下の点が重要な検討事項となるでしょう。

データ表示の複雑性 大量のデータを直感的に表示する必要があります。テーブル、チャート、カードなど様々な形式でのデータ可視化が求められます。

レスポンシブデザインの実装 デスクトップからモバイルまで、あらゆるデバイスで最適な表示を実現する必要があります。

ユーザビリティの確保 複雑な機能を持つダッシュボードでも、直感的な操作性を維持しなければなりません。

以下の図は、一般的なダッシュボードの構成要素を示します。

mermaidflowchart LR
    header[ヘッダー<br/>ナビゲーション] --> sidebar[サイドバー<br/>メニュー]
    header --> main[メインコンテンツ<br/>ダッシュボード本体]

    main --> cards[カード群<br/>KPI表示]
    main --> charts[チャート群<br/>データ可視化]
    main --> tables[テーブル群<br/>詳細データ]

    sidebar --> navigation[ページ遷移]
    sidebar --> filters[フィルタ機能]

なぜ shadcn/ui が選ばれるのか

shadcn/ui がダッシュボード開発で選ばれる理由は明確です。

開発効率の向上 すぐに使える高品質なコンポーネントが豊富に用意されています。ボタン、フォーム、モーダルなど、ダッシュボードに必要な要素がすべて揃っています。

一貫性のあるデザイン 統一されたデザインシステムにより、アプリケーション全体で一貫した見た目を実現できます。

メンテナンス性の高さ コンポーネントのソースコードを直接管理できるため、プロジェクト固有の要件に柔軟に対応できます。

課題

従来の UI 開発での問題点

従来の UI ライブラリを使用したダッシュボード開発では、いくつかの課題がありました。

カスタマイズの困難さ 多くの UI ライブラリは、デザインのカスタマイズが制限されていました。特に企業のブランドカラーや独自のデザイン要件に対応するのが困難でした。

typescript// 従来のライブラリでのカスタマイズ例
// CSS オーバーライドが必要で複雑
const customButton = {
  backgroundColor: '!important #007bff',
  border: '!important 1px solid #0056b3',
  '&:hover': {
    backgroundColor: '!important #0056b3',
  },
};

バンドルサイズの問題 使用しないコンポーネントも含めてライブラリ全体をインポートする必要があり、アプリケーションのサイズが肥大化していました。

アクセシビリティの不備 多くのライブラリでアクセシビリティ対応が不十分で、後から対応するのが困難でした。

ダッシュボード特有の設計課題

ダッシュボードアプリケーションには、一般的な Web サイトとは異なる固有の課題があります。

複雑なレイアウト管理 サイドバー、ヘッダー、メインコンテンツエリアなど、複数の領域を効率的に管理する必要があります。

以下の図は、複雑なダッシュボードレイアウトの課題を示しています。

mermaidstateDiagram-v2
    [*] --> モバイル表示
    [*] --> タブレット表示
    [*] --> デスクトップ表示

    モバイル表示 --> ハンバーガーメニュー
    タブレット表示 --> 縮小サイドバー
    デスクトップ表示 --> フルサイドバー

    ハンバーガーメニュー --> コンテンツ全幅
    縮小サイドバー --> コンテンツ調整
    フルサイドバー --> コンテンツ最適化

データローディング状態の管理 大量のデータを扱うダッシュボードでは、ローディング状態やエラー状態の適切な表示が重要です。

リアルタイム更新への対応 データが頻繁に更新されるダッシュボードでは、UI の再描画パフォーマンスが重要な要素となります。

レスポンシブ対応の難しさ

ダッシュボードのレスポンシブ対応は、特に複雑な課題です。

画面サイズによる情報優先度の変化 デスクトップでは多くの情報を同時表示できますが、モバイルでは重要な情報に絞って表示する必要があります。

インタラクション方法の違い マウス操作とタッチ操作では、最適な UI パターンが異なります。

チャートやグラフの表示調整 データ可視化要素は、画面サイズに応じて表示方法を大幅に変更する必要があります。

解決策

shadcn/ui を活用したコンポーネント設計

shadcn/ui を使用することで、これらの課題を効率的に解決できます。

コンポーネントベースアーキテクチャ まずは、shadcn/ui の基本的なコンポーネントをプロジェクトに導入します。

bash# shadcn/ui の初期化
npx shadcn-ui@latest init

プロジェクト設定後、必要なコンポーネントを個別にインストールします。

bash# ダッシュボードに必要な基本コンポーネント
npx shadcn-ui@latest add button
npx shadcn-ui@latest add card
npx shadcn-ui@latest add table
npx shadcn-ui@latest add sidebar

以下は基本的なコンポーネント構成の例です。

typescript// components/ui 配下にコンポーネントが配置される
import { Button } from '@/components/ui/button';
import {
  Card,
  CardHeader,
  CardTitle,
  CardContent,
} from '@/components/ui/card';
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from '@/components/ui/table';

再利用可能なカスタムコンポーネント shadcn/ui のベースコンポーネントを拡張して、プロジェクト固有のコンポーネントを作成します。

typescript// components/dashboard/MetricCard.tsx
import {
  Card,
  CardHeader,
  CardTitle,
  CardContent,
} from '@/components/ui/card';
import { LucideIcon } from 'lucide-react';

interface MetricCardProps {
  title: string;
  value: string | number;
  icon: LucideIcon;
  trend?: 'up' | 'down' | 'neutral';
  trendValue?: string;
}

export function MetricCard({
  title,
  value,
  icon: Icon,
  trend,
  trendValue,
}: MetricCardProps) {
  return (
    <Card>
      <CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
        <CardTitle className='text-sm font-medium'>
          {title}
        </CardTitle>
        <Icon className='h-4 w-4 text-muted-foreground' />
      </CardHeader>
      <CardContent>
        <div className='text-2xl font-bold'>{value}</div>
        {trend && trendValue && (
          <p
            className={`text-xs ${
              trend === 'up'
                ? 'text-green-600'
                : trend === 'down'
                ? 'text-red-600'
                : 'text-muted-foreground'
            }`}
          >
            {trendValue}
          </p>
        )}
      </CardContent>
    </Card>
  );
}

レイアウトの最適化手法

効率的なダッシュボードレイアウトを実現するため、shadcn/ui の柔軟性を活用します。

グリッドベースレイアウト CSS Grid と Tailwind CSS を組み合わせて、レスポンシブなグリッドレイアウトを構築します。

typescript// components/dashboard/DashboardGrid.tsx
interface DashboardGridProps {
  children: React.ReactNode;
}

export function DashboardGrid({
  children,
}: DashboardGridProps) {
  return (
    <div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 p-4'>
      {children}
    </div>
  );
}

以下の図は、レスポンシブグリッドレイアウトの動作を示します。

mermaidflowchart TD
    mobile[モバイル<br/>1列グリッド] --> tablet[タブレット<br/>2列グリッド]
    tablet --> desktop[デスクトップ<br/>3列グリッド]
    desktop --> wide[ワイドスクリーン<br/>4列グリッド]

    mobile --> |768px以上| tablet
    tablet --> |1024px以上| desktop
    desktop --> |1280px以上| wide

フレキシブルサイドバー shadcn/ui のサイドバーコンポーネントを活用して、適応的なナビゲーションを実装します。

typescript// components/dashboard/Sidebar.tsx
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import {
  Sheet,
  SheetContent,
  SheetTrigger,
} from '@/components/ui/sheet';
import { Menu } from 'lucide-react';

export function Sidebar() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <>
      {/* モバイル用ハンバーガーメニュー */}
      <Sheet open={isOpen} onOpenChange={setIsOpen}>
        <SheetTrigger asChild className='md:hidden'>
          <Button variant='outline' size='icon'>
            <Menu className='h-4 w-4' />
          </Button>
        </SheetTrigger>
        <SheetContent side='left' className='w-64'>
          <nav className='space-y-2'>
            {/* ナビゲーションアイテム */}
          </nav>
        </SheetContent>
      </Sheet>

      {/* デスクトップ用固定サイドバー */}
      <aside className='hidden md:flex w-64 flex-col border-r bg-background'>
        <nav className='flex-1 space-y-2 p-4'>
          {/* ナビゲーションアイテム */}
        </nav>
      </aside>
    </>
  );
}

状態管理との連携

shadcn/ui コンポーネントは、現代的な状態管理ライブラリとシームレスに連携します。

Zustand との組み合わせ 軽量な状態管理ライブラリ Zustand を使用した例です。

typescript// stores/dashboardStore.ts
import { create } from 'zustand';

interface DashboardState {
  metrics: {
    totalUsers: number;
    totalRevenue: number;
    conversionRate: number;
  };
  isLoading: boolean;
  fetchMetrics: () => Promise<void>;
}

export const useDashboardStore = create<DashboardState>(
  (set) => ({
    metrics: {
      totalUsers: 0,
      totalRevenue: 0,
      conversionRate: 0,
    },
    isLoading: false,
    fetchMetrics: async () => {
      set({ isLoading: true });
      try {
        // API コール
        const response = await fetch('/api/metrics');
        const data = await response.json();
        set({ metrics: data, isLoading: false });
      } catch (error) {
        set({ isLoading: false });
      }
    },
  })
);

React Query との統合 データフェッチングには React Query を活用します。

typescript// hooks/useMetrics.ts
import { useQuery } from '@tanstack/react-query';

export function useMetrics() {
  return useQuery({
    queryKey: ['metrics'],
    queryFn: async () => {
      const response = await fetch('/api/metrics');
      if (!response.ok) {
        throw new Error('メトリクスの取得に失敗しました');
      }
      return response.json();
    },
    refetchInterval: 30000, // 30秒ごとに自動更新
  });
}

アクセシビリティ対応

shadcn/ui は Radix UI ベースのため、優れたアクセシビリティサポートを提供します。

キーボードナビゲーション すべてのインタラクティブ要素がキーボードで操作可能です。

typescript// components/dashboard/AccessibleButton.tsx
import { Button } from '@/components/ui/button';

export function AccessibleButton({
  children,
  onClick,
  ...props
}) {
  return (
    <Button
      onClick={onClick}
      onKeyDown={(e) => {
        if (e.key === 'Enter' || e.key === ' ') {
          e.preventDefault();
          onClick();
        }
      }}
      {...props}
    >
      {children}
    </Button>
  );
}

ARIA ラベルの適切な使用 スクリーンリーダー対応のため、適切な ARIA ラベルを設定します。

typescript// components/dashboard/MetricWithAria.tsx
export function MetricWithAria({
  title,
  value,
  description,
}) {
  return (
    <div
      role='region'
      aria-labelledby='metric-title'
      aria-describedby='metric-description'
    >
      <h3 id='metric-title'>{title}</h3>
      <div aria-live='polite'>{value}</div>
      <p id='metric-description' className='sr-only'>
        {description}
      </p>
    </div>
  );
}

具体例

基本的なダッシュボードレイアウト

実際のダッシュボード実装例をご紹介します。まずは基本的なレイアウト構造から始めましょう。

typescript// app/dashboard/page.tsx
import { Sidebar } from '@/components/dashboard/Sidebar';
import { Header } from '@/components/dashboard/Header';
import { MetricCard } from '@/components/dashboard/MetricCard';
import { DashboardGrid } from '@/components/dashboard/DashboardGrid';

export default function DashboardPage() {
  return (
    <div className='flex min-h-screen bg-background'>
      <Sidebar />

      <div className='flex-1 flex flex-col'>
        <Header />

        <main className='flex-1 p-6'>
          <div className='space-y-6'>
            <div>
              <h1 className='text-3xl font-bold tracking-tight'>
                ダッシュボード
              </h1>
              <p className='text-muted-foreground'>
                プロジェクトの概要と主要指標をご確認ください
              </p>
            </div>

            <DashboardGrid>
              <MetricCard
                title='総ユーザー数'
                value='12,345'
                icon={Users}
                trend='up'
                trendValue='+12.5% 前月比'
              />
              {/* 他のメトリクス */}
            </DashboardGrid>
          </div>
        </main>
      </div>
    </div>
  );
}

以下の図は、基本的なダッシュボードレイアウトの構造を示します。

mermaidflowchart TB
    subgraph dashboard [ダッシュボード全体]
        subgraph sidebar [サイドバー]
            nav[ナビゲーション]
            menu[メニューアイテム]
        end

        subgraph main [メインエリア]
            header[ヘッダー]
            content[コンテンツエリア]
        end

        subgraph content [コンテンツエリア]
            title[ページタイトル]
            grid[メトリクスグリッド]
            charts[チャート群]
        end
    end

データ表示コンポーネントの実装

shadcn/ui のテーブルコンポーネントを活用して、効率的なデータ表示を実現します。

typescript// components/dashboard/DataTable.tsx
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from '@/components/ui/table';
import { Badge } from '@/components/ui/badge';

interface User {
  id: string;
  name: string;
  email: string;
  status: 'active' | 'inactive';
  lastLogin: string;
}

interface DataTableProps {
  data: User[];
}

export function DataTable({ data }: DataTableProps) {
  return (
    <div className='rounded-md border'>
      <Table>
        <TableHeader>
          <TableRow>
            <TableHead>名前</TableHead>
            <TableHead>メールアドレス</TableHead>
            <TableHead>ステータス</TableHead>
            <TableHead>最終ログイン</TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          {data.map((user) => (
            <TableRow key={user.id}>
              <TableCell className='font-medium'>
                {user.name}
              </TableCell>
              <TableCell>{user.email}</TableCell>
              <TableCell>
                <Badge
                  variant={
                    user.status === 'active'
                      ? 'default'
                      : 'secondary'
                  }
                >
                  {user.status === 'active'
                    ? 'アクティブ'
                    : '非アクティブ'}
                </Badge>
              </TableCell>
              <TableCell>{user.lastLogin}</TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </div>
  );
}

ソート機能付きテーブル より高度な機能として、ソート機能を追加します。

typescript// components/dashboard/SortableTable.tsx
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import {
  ArrowUpDown,
  ArrowUp,
  ArrowDown,
} from 'lucide-react';

type SortDirection = 'asc' | 'desc' | null;

export function SortableTable({ data, columns }) {
  const [sortColumn, setSortColumn] = useState<
    string | null
  >(null);
  const [sortDirection, setSortDirection] =
    useState<SortDirection>(null);

  const handleSort = (columnKey: string) => {
    if (sortColumn === columnKey) {
      setSortDirection(
        sortDirection === 'asc'
          ? 'desc'
          : sortDirection === 'desc'
          ? null
          : 'asc'
      );
    } else {
      setSortColumn(columnKey);
      setSortDirection('asc');
    }
  };

  const getSortIcon = (columnKey: string) => {
    if (sortColumn !== columnKey)
      return <ArrowUpDown className='ml-2 h-4 w-4' />;
    if (sortDirection === 'asc')
      return <ArrowUp className='ml-2 h-4 w-4' />;
    if (sortDirection === 'desc')
      return <ArrowDown className='ml-2 h-4 w-4' />;
    return <ArrowUpDown className='ml-2 h-4 w-4' />;
  };

  return (
    <Table>
      <TableHeader>
        <TableRow>
          {columns.map((column) => (
            <TableHead key={column.key}>
              <Button
                variant='ghost'
                onClick={() => handleSort(column.key)}
                className='h-auto p-0 font-semibold'
              >
                {column.label}
                {getSortIcon(column.key)}
              </Button>
            </TableHead>
          ))}
        </TableRow>
      </TableHeader>
      {/* テーブルボディの実装 */}
    </Table>
  );
}

ナビゲーション設計

効果的なナビゲーション設計は、ダッシュボードのユーザビリティに大きく影響します。

typescript// components/dashboard/Navigation.tsx
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import {
  Home,
  BarChart3,
  Users,
  Settings,
  FileText,
} from 'lucide-react';

const navigationItems = [
  {
    title: 'ダッシュボード',
    href: '/dashboard',
    icon: Home,
  },
  {
    title: 'アナリティクス',
    href: '/dashboard/analytics',
    icon: BarChart3,
  },
  {
    title: 'ユーザー管理',
    href: '/dashboard/users',
    icon: Users,
  },
  {
    title: 'レポート',
    href: '/dashboard/reports',
    icon: FileText,
  },
  {
    title: '設定',
    href: '/dashboard/settings',
    icon: Settings,
  },
];

interface NavigationProps {
  currentPath: string;
}

export function Navigation({
  currentPath,
}: NavigationProps) {
  return (
    <nav className='space-y-2'>
      {navigationItems.map((item) => {
        const Icon = item.icon;
        const isActive = currentPath === item.href;

        return (
          <Button
            key={item.href}
            variant={isActive ? 'secondary' : 'ghost'}
            className={cn(
              'w-full justify-start',
              isActive && 'bg-secondary'
            )}
            asChild
          >
            <a href={item.href}>
              <Icon className='mr-2 h-4 w-4' />
              {item.title}
            </a>
          </Button>
        );
      })}
    </nav>
  );
}

以下の図は、ナビゲーション設計の考え方を示します。

mermaidflowchart LR
    subgraph nav [ナビゲーション構造]
        primary[プライマリメニュー]
        secondary[セカンダリメニュー]

        primary --> dashboard[ダッシュボード]
        primary --> analytics[アナリティクス]
        primary --> users[ユーザー管理]

        secondary --> settings[設定]
        secondary --> help[ヘルプ]
    end

    dashboard --> overview[概要]
    dashboard --> metrics[メトリクス]

    analytics --> reports[レポート]
    analytics --> insights[インサイト]

チャート・グラフの組み込み

データ可視化には、Recharts ライブラリと shadcn/ui を組み合わせて使用します。

bash# Recharts のインストール
yarn add recharts
yarn add -D @types/recharts

基本的なチャートコンポーネントを作成します。

typescript// components/dashboard/AreaChart.tsx
import {
  ResponsiveContainer,
  AreaChart as RechartsAreaChart,
  Area,
  XAxis,
  YAxis,
  CartesianGrid,
  Tooltip,
} from 'recharts';
import {
  Card,
  CardHeader,
  CardTitle,
  CardContent,
} from '@/components/ui/card';

interface ChartDataPoint {
  name: string;
  value: number;
}

interface AreaChartProps {
  title: string;
  data: ChartDataPoint[];
}

export function AreaChart({ title, data }: AreaChartProps) {
  return (
    <Card>
      <CardHeader>
        <CardTitle>{title}</CardTitle>
      </CardHeader>
      <CardContent>
        <ResponsiveContainer width='100%' height={300}>
          <RechartsAreaChart data={data}>
            <CartesianGrid strokeDasharray='3 3' />
            <XAxis dataKey='name' />
            <YAxis />
            <Tooltip
              contentStyle={{
                backgroundColor: 'hsl(var(--background))',
                border: '1px solid hsl(var(--border))',
                borderRadius: '6px',
              }}
            />
            <Area
              type='monotone'
              dataKey='value'
              stroke='hsl(var(--primary))'
              fill='hsl(var(--primary))'
              fillOpacity={0.2}
            />
          </RechartsAreaChart>
        </ResponsiveContainer>
      </CardContent>
    </Card>
  );
}

複数系列のチャート 複数のデータ系列を表示するチャートも簡単に実装できます。

typescript// components/dashboard/MultiLineChart.tsx
import {
  ResponsiveContainer,
  LineChart,
  Line,
  XAxis,
  YAxis,
  CartesianGrid,
  Tooltip,
  Legend,
} from 'recharts';

interface MultiLineChartProps {
  title: string;
  data: Array<{
    name: string;
    users: number;
    revenue: number;
  }>;
}

export function MultiLineChart({
  title,
  data,
}: MultiLineChartProps) {
  return (
    <Card>
      <CardHeader>
        <CardTitle>{title}</CardTitle>
      </CardHeader>
      <CardContent>
        <ResponsiveContainer width='100%' height={300}>
          <LineChart data={data}>
            <CartesianGrid strokeDasharray='3 3' />
            <XAxis dataKey='name' />
            <YAxis />
            <Tooltip />
            <Legend />
            <Line
              type='monotone'
              dataKey='users'
              stroke='hsl(var(--primary))'
              strokeWidth={2}
              name='ユーザー数'
            />
            <Line
              type='monotone'
              dataKey='revenue'
              stroke='hsl(var(--destructive))'
              strokeWidth={2}
              name='売上'
            />
          </LineChart>
        </ResponsiveContainer>
      </CardContent>
    </Card>
  );
}

図で理解できる要点

  • チャートは shadcn/ui のカードコンポーネントで統一感を保つ
  • Recharts のテーマは CSS カスタムプロパティで shadcn/ui と同期
  • レスポンシブコンテナで画面サイズに自動対応

まとめ

shadcn/ui を活用したダッシュボード開発は、現代の Web アプリケーション開発において最適なアプローチの一つです。

主な利点の再確認

  • 開発効率: 高品質なコンポーネントですぐに開発開始
  • カスタマイズ性: プロジェクト要件に合わせた柔軟な調整
  • アクセシビリティ: Radix UI ベースの優れたアクセシビリティサポート
  • 保守性: TypeScript による型安全性とコードの可読性

実装のポイント 段階的なアプローチで実装を進めることが重要です。まずは基本的なレイアウトから始めて、徐々に高度な機能を追加していきましょう。

今後の発展 shadcn/ui のエコシステムは継続的に発展しており、新しいコンポーネントや機能が定期的に追加されています。公式ドキュメントをチェックして、最新の機能を活用していくことをおすすめします。

効率的で美しいダッシュボードの構築により、ユーザーエクスペリエンスの向上と開発チームの生産性向上の両方を実現できるでしょう。

関連リンク