Base UI by MUI実践ガイド - スタイルレスUIで自由なデザインシステム構築
MUIから独立したスタイルレスコンポーネントライブラリBase UIの使い方を徹底解説。アクセシビリティを保ちながら独自のデザインシステムを構築する方法を実例付きで紹介します。
MUIの新しいBase UIでヘッドレスコンポーネントを活用したデザインシステムの構築方法を実践解説。Radix UI、Headless UIとの比較から実装例、移行戦略まで包括的にカバーします。
MUI チームが開発する Base UI は、スタイルが適用されていないヘッドレスコンポーネントライブラリです。従来の Material UI とは異なり、ロジックとアクセシビリティのみを提供し、スタイリングは開発者に委ねられます。本記事では、Base UI の実践的な実装方法、他のヘッドレス UI ライブラリとの比較、そして実際のプロジェクトでの活用方法を解説します。
Base UI(旧@mui/base)は、MUI チームが開発したヘッドレス(スタイルなし)コンポーネントライブラリです。従来の Material UI が提供するスタイル付きコンポーネントとは対照的に、Base UI は機能とアクセシビリティのみを提供し、見た目は完全に開発者に委ねられます。
特徴 | MUI Material | Base UI | 利点 |
---|---|---|---|
スタイリング | Material Design準拠 | スタイルなし | 完全な自由度 |
バンドルサイズ | 大(スタイル含む) | 小(ロジックのみ) | 20-30%削減 |
カスタマイズ性 | テーマ制限あり | 無制限 | 独自デザイン |
学習コスト | 低 | 中 | CSS知識必要 |
開発速度 | 高速(プロトタイプ) | 中速(カスタム) | 用途次第 |
保守性 | 中 | 高 | デザイン変更容易 |
チャートを読み込み中...
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>
);
}
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>
);
}
ライブラリ | コンポーネント数 | バンドルサイズ | フレームワーク | アクセシビリティ | 学習コスト |
---|---|---|---|---|---|
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 | ★★★★☆ | ★★★☆☆ |
課題: 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>
);
}
結果: 独自のデザインでブランドアイデンティティを確立、ユーザーの混乱を減らした
Base UI の組み込みアクセシビリティ機能により、WCAG 2.1 AA 準拠を短期間で達成できました。特にキーボードナビゲーションとスクリーンリーダー対応が素晴らしく、手動での実装コストを大幅に削減できました。
// 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';
クラス名の衝突を防ぎ、保守性向上
JavaScript内でスタイル定義、動的スタイリング
高速開発、一貫性のあるデザイン
シンプル、学習コスト最小
// 移行フェーズ管理の例
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'
}
};
Base UIの進化予測:
Base UI by MUI は、2025 年のフロントエンド開発において、デザインシステム構築の新しいアプローチを提案しています。完全にスタイルレスなコンポーネントにより、開発者は究極の自由度を手に入れることができます。
Base UI採用のメリット:
2025年後半の展望:
ヘッドレス UI ライブラリの選択は、プロジェクトの要件とチームのスキルレベルに依存します。Base UI は、特に Material UI の経験があるチームにとって、デザインの自由度を保ちながら開発効率を維持できる優れた選択肢となるでしょう。