ブログ記事

Base UI by MUI実践ガイド2025 - スタイルレスUIで自由なデザインシステム構築

MUIの新しいBase UIでヘッドレスコンポーネントを活用したデザインシステムの構築方法を実践解説。Radix UI、Headless UIとの比較から実装例、移行戦略まで包括的にカバーします。

9分で読めます
R
Rina
Daily Hack 編集長
Web開発
MUI Base UI ヘッドレスUI デザインシステム React UI/UX
Base UI by MUI実践ガイド2025 - スタイルレスUIで自由なデザインシステム構築のヒーロー画像

MUI チームが開発する Base UI は、スタイルが適用されていないヘッドレスコンポーネントライブラリです。従来の Material UI とは異なり、ロジックとアクセシビリティのみを提供し、スタイリングは開発者に委ねられます。本記事では、Base UI の実践的な実装方法、他のヘッドレス UI ライブラリとの比較、そして実際のプロジェクトでの活用方法を解説します。

この記事で学べること

  • Base UI の基本概念と従来の MUI Material との違い
  • ヘッドレス UI アーキテクチャの利点と活用法
  • 実践的なコンポーネント実装とカスタマイズ手法
  • Radix UI、Headless UI との詳細比較
  • 既存プロジェクトからの段階的移行戦略

Base UIとは何か

ヘッドレスUIのコンセプト

Base UI(旧@mui/base)は、MUI チームが開発したヘッドレス(スタイルなし)コンポーネントライブラリです。従来の Material UI が提供するスタイル付きコンポーネントとは対照的に、Base UI は機能とアクセシビリティのみを提供し、見た目は完全に開発者に委ねられます。

MUI MaterialとBase UIの詳細比較
特徴 MUI Material Base UI 利点
スタイリング Material Design準拠 スタイルなし 完全な自由度
バンドルサイズ 大(スタイル含む) 小(ロジックのみ) 20-30%削減
カスタマイズ性 テーマ制限あり 無制限 独自デザイン
学習コスト CSS知識必要
開発速度 高速(プロトタイプ) 中速(カスタム) 用途次第
保守性 デザイン変更容易

アーキテクチャの進化

Base UIアーキテクチャ図

チャートを読み込み中...

実践的なコンポーネント実装

基本的なボタンコンポーネント

Base UI の力を実感できる基本的な実装から始めましょう:

import { Button } from '@mui/base/Button';
import { styled } from '@mui/system';

// スタイル付きボタンの作成
const StyledButton = styled(Button)(({ theme, variant }) => ({
  fontFamily: 'Inter, sans-serif',
  fontWeight: 600,
  fontSize: '0.875rem',
  lineHeight: 1.5,
  padding: '8px 16px',
  borderRadius: '8px',
  transition: 'all 150ms ease',
  cursor: 'pointer',
  border: 'none',
  
  ...(variant === 'primary' && {
    backgroundColor: '#1976d2',
    color: 'white',
    '&:hover': {
      backgroundColor: '#1565c0',
    },
    '&:active': {
      backgroundColor: '#0d47a1',
    },
  }),
  
  ...(variant === 'secondary' && {
    backgroundColor: 'transparent',
    color: '#1976d2',
    border: '1px solid #1976d2',
    '&:hover': {
      backgroundColor: '#e3f2fd',
    },
  }),
  
  '&:disabled': {
    opacity: 0.6,
    cursor: 'not-allowed',
  },
}));

// 使用例
export default function CustomButton({ variant = 'primary', children, ...props }) {
  return (
    <StyledButton variant={variant} {...props}>
      {children}
    </StyledButton>
  );
}
import { useButton } from '@mui/base/useButton';
import { forwardRef } from 'react';

// useButtonフックを直接使用
const CustomButton = forwardRef(function CustomButton(props, ref) {
  const { children, disabled, ...otherProps } = props;
  
  const { getRootProps, active, disabled: isDisabled } = useButton({
    disabled,
    rootRef: ref,
  });

  const classes = `
    inline-flex items-center justify-center
    px-4 py-2 rounded-md font-medium
    transition-all duration-150 ease-in-out
    focus:outline-none focus:ring-2 focus:ring-blue-500
    ${isDisabled 
      ? 'opacity-50 cursor-not-allowed bg-gray-300 text-gray-500' 
      : 'bg-blue-600 text-white hover:bg-blue-700'
    }
    ${active ? 'transform scale-95' : ''}
  `;

  return (
    <button type="button" {...getRootProps(otherProps)} className={classes}>
      {children}
    </button>
  );
});

export default CustomButton;
import { Button } from '@mui/base/Button';
import clsx from 'clsx';

const buttonClasses = {
  base: 'inline-flex items-center justify-center font-medium rounded-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2',
  variants: {
    primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
    secondary: 'bg-white text-blue-600 border border-blue-600 hover:bg-blue-50 focus:ring-blue-500',
    danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
  },
  sizes: {
    sm: 'px-3 py-1.5 text-sm',
    md: 'px-4 py-2 text-base',
    lg: 'px-6 py-3 text-lg',
  },
  disabled: 'opacity-50 cursor-not-allowed',
};

export default function TailwindButton({ 
  variant = 'primary', 
  size = 'md', 
  disabled = false,
  className,
  children,
  ...props 
}) {
  const buttonClassName = clsx(
    buttonClasses.base,
    buttonClasses.variants[variant],
    buttonClasses.sizes[size],
    disabled && buttonClasses.disabled,
    className
  );

  return (
    <Button className={buttonClassName} disabled={disabled} {...props}>
      {children}
    </Button>
  );
}

複雑なコンポーネントの実装

import { Switch, FormControlLabel } from '@mui/material';
import { createTheme, ThemeProvider } from '@mui/material/styles';

// テーマでカスタマイズ(制限あり)
const theme = createTheme({
  components: {
    MuiSwitch: {
      styleOverrides: {
        root: {
          width: 46,
          height: 27,
          padding: 0,
        },
        switchBase: {
          padding: 1,
          '&.Mui-checked': {
            color: '#fff',
            transform: 'translateX(19px)',
          },
        },
      },
    },
  },
});

function MaterialSwitch() {
  return (
    <ThemeProvider theme={theme}>
      <FormControlLabel
        control={<Switch />}
        label="ダークモード"
      />
    </ThemeProvider>
  );
}
import { Switch } from '@mui/base/Switch';
import { styled } from '@mui/system';

// 完全にカスタムなスタイリング
const CustomSwitch = styled(Switch)(({ theme }) => ({
  width: 60,
  height: 34,
  padding: 0,
  display: 'flex',
  
  '& .base-Switch-input': {
    cursor: 'inherit',
    position: 'absolute',
    width: '100%',
    height: '100%',
    top: 0,
    left: 0,
    opacity: 0,
    zIndex: 1,
    margin: 0,
  },
  
  '& .base-Switch-track': {
    backgroundColor: '#b3b3b3',
    borderRadius: 34 / 2,
    width: '100%',
    height: '100%',
    display: 'block',
    transition: 'background-color 0.2s',
  },
  
  '& .base-Switch-thumb': {
    display: 'block',
    width: 26,
    height: 26,
    backgroundColor: '#fff',
    borderRadius: '50%',
    position: 'absolute',
    top: 4,
    left: 4,
    transition: 'transform 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
    boxShadow: '0 2px 4px rgba(0, 0, 0, 0.2)',
  },
  
  '&.base-Switch-checked': {
    '& .base-Switch-track': {
      backgroundColor: '#4CAF50',
    },
    '& .base-Switch-thumb': {
      transform: 'translateX(26px)',
    },
  },
  
  '&:hover .base-Switch-thumb': {
    boxShadow: '0 0 0 8px rgba(76, 175, 80, 0.16)',
  },
}));

function BaseUISwitch() {
  return (
    <div className="flex items-center space-x-3">
      <CustomSwitch />
      <span className="text-gray-700">ダークモード</span>
    </div>
  );
}
従来のMaterial UI実装
import { Switch, FormControlLabel } from '@mui/material';
import { createTheme, ThemeProvider } from '@mui/material/styles';

// テーマでカスタマイズ(制限あり)
const theme = createTheme({
  components: {
    MuiSwitch: {
      styleOverrides: {
        root: {
          width: 46,
          height: 27,
          padding: 0,
        },
        switchBase: {
          padding: 1,
          '&.Mui-checked': {
            color: '#fff',
            transform: 'translateX(19px)',
          },
        },
      },
    },
  },
});

function MaterialSwitch() {
  return (
    <ThemeProvider theme={theme}>
      <FormControlLabel
        control={<Switch />}
        label="ダークモード"
      />
    </ThemeProvider>
  );
}
Base UIを使った柔軟な実装
import { Switch } from '@mui/base/Switch';
import { styled } from '@mui/system';

// 完全にカスタムなスタイリング
const CustomSwitch = styled(Switch)(({ theme }) => ({
  width: 60,
  height: 34,
  padding: 0,
  display: 'flex',
  
  '& .base-Switch-input': {
    cursor: 'inherit',
    position: 'absolute',
    width: '100%',
    height: '100%',
    top: 0,
    left: 0,
    opacity: 0,
    zIndex: 1,
    margin: 0,
  },
  
  '& .base-Switch-track': {
    backgroundColor: '#b3b3b3',
    borderRadius: 34 / 2,
    width: '100%',
    height: '100%',
    display: 'block',
    transition: 'background-color 0.2s',
  },
  
  '& .base-Switch-thumb': {
    display: 'block',
    width: 26,
    height: 26,
    backgroundColor: '#fff',
    borderRadius: '50%',
    position: 'absolute',
    top: 4,
    left: 4,
    transition: 'transform 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
    boxShadow: '0 2px 4px rgba(0, 0, 0, 0.2)',
  },
  
  '&.base-Switch-checked': {
    '& .base-Switch-track': {
      backgroundColor: '#4CAF50',
    },
    '& .base-Switch-thumb': {
      transform: 'translateX(26px)',
    },
  },
  
  '&:hover .base-Switch-thumb': {
    boxShadow: '0 0 0 8px rgba(76, 175, 80, 0.16)',
  },
}));

function BaseUISwitch() {
  return (
    <div className="flex items-center space-x-3">
      <CustomSwitch />
      <span className="text-gray-700">ダークモード</span>
    </div>
  );
}

ヘッドレスUIライブラリ比較分析

主要ライブラリの詳細比較

ヘッドレスUIライブラリの包括的比較(2025年版)
ライブラリ コンポーネント数 バンドルサイズ フレームワーク アクセシビリティ 学習コスト
Base UI 15+ (開発中) ~25KB React ★★★★★ ★★★☆☆
Radix UI 30+ ~35KB React ★★★★★ ★★★★☆
Headless UI 16 ~20KB React/Vue ★★★★☆ ★★★★★
Ark UI 35+ ~40KB React/Vue/Solid ★★★★☆ ★★☆☆☆
React Aria 50+ ~60KB React ★★★★★ ★★☆☆☆
Mantine Unstyled 40+ ~45KB React ★★★★☆ ★★★☆☆

用途別推奨度分析

Base UI - Material UIからの移行 90 %
Radix UI - 新規Reactプロジェクト 95 %
Headless UI - Tailwind CSSプロジェクト 85 %
Ark UI - マルチフレームワーク要件 80 %

実世界での活用事例

ケーススタディ1: SaaSダッシュボードの構築

課題: Material UI のデフォルトデザインから逸脫したい

Base UIソリューション:

// カスタムデータテーブルコンポーネント
import { Table } from '@mui/base/Table';
import { TableBody } from '@mui/base/TableBody';
import { TableCell } from '@mui/base/TableCell';
import { TableHead } from '@mui/base/TableHead';
import { TableRow } from '@mui/base/TableRow';

const StyledTable = styled(Table)`
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  border-radius: 12px;
  overflow: hidden;
  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
`;

const StyledTableHead = styled(TableHead)`
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
`;

const StyledHeaderCell = styled(TableCell)`
  color: white;
  font-weight: 600;
  padding: 16px 24px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  border: none;
`;

const StyledTableRow = styled(TableRow)`
  background-color: white;
  transition: all 0.2s ease;
  
  &:nth-of-type(even) {
    background-color: #f8fafc;
  }
  
  &:hover {
    background-color: #e2e8f0;
    transform: translateY(-1px);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  }
`;

function CustomDataTable({ data, columns }) {
  return (
    <StyledTable>
      <StyledTableHead>
        <TableRow>
          {columns.map((column) => (
            <StyledHeaderCell key={column.key}>
              {column.title}
            </StyledHeaderCell>
          ))}
        </TableRow>
      </StyledTableHead>
      <TableBody>
        {data.map((row, index) => (
          <StyledTableRow key={index}>
            {columns.map((column) => (
              <TableCell key={column.key} style={{ padding: '16px 24px' }}>
                {row[column.key]}
              </TableCell>
            ))}
          </StyledTableRow>
        ))}
      </TableBody>
    </StyledTable>
  );
}

結果: 独自のデザインでブランドアイデンティティを確立、ユーザーの混乱を減らした

ケーススタディ2: アクセシビリティ重視のECサイト

Base UI の組み込みアクセシビリティ機能により、WCAG 2.1 AA 準拠を短期間で達成できました。特にキーボードナビゲーションとスクリーンリーダー対応が素晴らしく、手動での実装コストを大幅に削減できました。

フロントエンドエンジニア 大手ECサイト

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

バンドルサイズ最適化

// Tree Shakingを活用した最適なインポート
import { Button } from '@mui/base/Button';
import { Switch } from '@mui/base/Switch';
import { Slider } from '@mui/base/Slider';

// 避けるべき: 全体インポート
// import * as Base from '@mui/base';

// 推奨: 必要なコンポーネントのみインポート
import { 
  Button,
  Switch, 
  Slider,
  useButton,
  useSwitch 
} from '@mui/base';

スタイリング戦略の比較

ローカルスコープCSS

クラス名の衝突を防ぎ、保守性向上

CSS-in-JS

JavaScript内でスタイル定義、動的スタイリング

ユーティリティファースト

高速開発、一貫性のあるデザイン

従来のCSS

シンプル、学習コスト最小

移行戦略とロードマップ

Material UIからBase UIへの段階的移行

移行のベストプラクティス

  1. 段階的置き換え: 一度にすべてを変更せず、コンポーネント単位で移行
  2. 共存期間の設定: Material UI と Base UI を一時的に並行使用
  3. スタイルガイドの準備: 新しいデザインシステムの文書化
  4. チーム教育: Base UI の概念とベストプラクティスの共有
// 移行フェーズ管理の例
type MigrationPhase = 'material-ui' | 'hybrid' | 'base-ui';

interface ComponentConfig {
  phase: MigrationPhase;
  component: React.ComponentType<any>;
  migrationPriority: 'high' | 'medium' | 'low';
}

const componentRegistry: Record<string, ComponentConfig> = {
  Button: {
    phase: 'base-ui', // 移行完了
    component: CustomBaseButton,
    migrationPriority: 'high'
  },
  DataGrid: {
    phase: 'hybrid', // 移行中
    component: MaterialDataGrid, // 一時的にMaterial UIを使用
    migrationPriority: 'medium'
  },
  DatePicker: {
    phase: 'material-ui', // 未移行
    component: MaterialDatePicker,
    migrationPriority: 'low'
  }
};

2025年の技術トレンドと将来性

Base UIの進化予測:

  • 新しいBase UI(Radixとのコラボ): より成熟した API と豊富なコンポーネント
  • React Server Components対応: SSR での最適化
  • Accessibility向上: より詳細な ARIA 実装
  • Performance強化: バンドルサイズの更なる削減

トラブルシューティングとFAQ

よくある問題と解決策

注意すべき点

  • スタイルの継承: Base UI はスタイルを提供しないため、すべて自分で実装
  • ブラウザサポート: モダンブラウザ推奨(IE11 非対応)
  • 学習コスト: CSS 知識とアクセシビリティの理解が必要
  • ドキュメント: 新しい Base UI はまだドキュメントが発展途上

まとめと今後の展望

Base UI by MUI は、2025 年のフロントエンド開発において、デザインシステム構築の新しいアプローチを提案しています。完全にスタイルレスなコンポーネントにより、開発者は究極の自由度を手に入れることができます。

Base UI採用のメリット:

  • 完全なデザインコントロール
  • 軽量なバンドルサイズ
  • 堅牢なアクセシビリティ機能
  • Material UI エコシステムとの親和性

2025年後半の展望:

  • Radix チームとの協力による新 Base UI の正式リリース
  • より豊富なコンポーネントライブラリ
  • React 19 との完全統合
  • サーバーサイドレンダリングの最適化

ヘッドレス UI ライブラリの選択は、プロジェクトの要件とチームのスキルレベルに依存します。Base UI は、特に Material UI の経験があるチームにとって、デザインの自由度を保ちながら開発効率を維持できる優れた選択肢となるでしょう。

Rinaのプロフィール画像

Rina

Daily Hack 編集長

フルスタックエンジニアとして10年以上の経験を持つ。 大手IT企業やスタートアップでの開発経験を活かし、 実践的で即効性のある技術情報を日々発信中。 特にWeb開発、クラウド技術、AI活用に精通。

この記事は役に立ちましたか?

あなたのフィードバックが記事の改善に役立ちます

この記事は役に立ちましたか?

Daily Hackでは、開発者の皆様に役立つ情報を毎日発信しています。