テーマ
@nodeblocks/frontend-theme は、Nodeblocks のフロントエンドブロックで使われる共有の Material UI デザインシステムです。事前設定済みの defaultTheme、グローバルスタイルと CSS ベースラインを登録するルートの ThemeProvider、およびアプリのブランディング時にマージできるトークンモジュール(palette、typography、spacing、components)を提供します。
ThemeProvider をアプリケーションのルートに 1 回だけマウントすると、すべてのブロックと MUI コンポーネントが同じ色、タイポグラフィ、間隔、シャドウ、コンポーネントごとのスタイルオーバーライドを共有します。
インストール
npm install @nodeblocks/frontend-theme @mui/material @emotion/react @emotion/styled
使い方
アプリ(または Storybook のプレビュー)を ThemeProvider でラップします。すべてのフロントエンドブロックはこのコンテキストが存在することを前提としています。
import { ThemeProvider, defaultTheme } from '@nodeblocks/frontend-theme';
export function App() {
return (
<ThemeProvider theme={defaultTheme}>
{/* ルート、ブロック、その他の MUI UI */}
</ThemeProvider>
);
}
ThemeProvider は次を受け取ります。
theme— MUI テーマオブジェクト(デフォルト:defaultTheme)enableCssBaseline— MUI のCssBaselineをレンダリングする(デフォルト:true)injectMuiStylesFirst— カスタム CSS が MUI より優先されるようにする(デフォルト:true)
テーマのカスタマイズ
defaultTheme にオーバーライドをマージすると、既存のコンポーネントスタイルを維持したまま、必要な部分だけを変更できます。
パレットとトークン
import { createTheme } from '@mui/material';
import { ThemeProvider, defaultTheme } from '@nodeblocks/frontend-theme';
const brandTheme = createTheme(defaultTheme, {
palette: {
primary: {
main: '#2e7d32',
light: '#4caf50',
dark: '#1b5e20',
contrastText: '#ffffff',
},
},
});
<ThemeProvider theme={brandTheme}>{/* ... */}</ThemeProvider>;
コンポーネントのオーバーライド — エクスポートされた components マップをスプレッドし、個別のキーをパッチします。
import { createTheme } from '@mui/material';
import { ThemeProvider, defaultTheme, components } from '@nodeblocks/frontend-theme';
const customTheme = createTheme(defaultTheme, {
components: {
...components,
MuiButton: {
styleOverrides: {
root: { borderRadius: 12 },
},
},
},
});
エクスポートされたモジュールから再構築
import { createTheme } from '@mui/material';
import { palette, typography, spacing, components, defaultTheme } from '@nodeblocks/frontend-theme';
const theme = createTheme(defaultTheme, {
palette: { ...palette, primary: { ...palette.primary, main: '#006ead' } },
typography,
spacing,
components,
});
エクスポート API: defaultTheme、ThemeProvider、useTheme、GlobalStyles、palette、typography、spacing、components、ThemeProviderProps。
テーマ適用コンポーネント
ThemeProvider を省略このページの tsx live スニペットはすべて、内部で defaultTheme の ThemeProvider でプレビューをラップするドキュメント Playground 内でレンダリングされます。そのため例ではラッパーを省略し、UI を読みやすくしています。独自のアプリケーションでは、使い方 で示したとおり、ルートに ThemeProvider をマウントする必要があります。
以下のセクションは、blocks/frontend-theme の Storybook プレビュー(SharedTheme.stories.tsx)に対応しています。各スニペットは components.ts でテーマオーバーライドがある MUI コンポーネントのスタイル出力を示す tsx live 例です。
カラーパレット
パレットトークンは primary、secondary、セマンティックカラー、背景、テキストを制御します。
function Example() { const colors = ['primary', 'secondary', 'error', 'warning', 'info', 'success']; const shades = ['main', 'light', 'dark']; return ( <Stack direction="row" spacing={2} sx={{ p: 3, flexWrap: 'wrap', gap: 2 }}> {colors.map((color) => ( <Card key={color} sx={{ minWidth: 160 }}> <CardContent> <Typography variant="h6" gutterBottom sx={{ textTransform: 'capitalize' }}> {color} </Typography> <Stack direction="row" spacing={1}> {shades.map((shade) => ( <Box key={shade} sx={{ width: 48, height: 48, bgcolor: `${color}.${shade}`, borderRadius: 1 }} title={`${color}.${shade}`} /> ))} </Stack> </CardContent> </Card> ))} </Stack> ); }
グレースケール
拡張された grey の段階(25、75、50–900)がパレットで定義されています。
function Example() { const shades = ['25', '50', '75', '100', '200', '300', '400', '500', '600', '700', '800', '900']; return ( <Stack spacing={1} sx={{ p: 3, maxWidth: 400 }}> {shades.map((shade) => ( <Stack key={shade} direction="row" spacing={2} alignItems="center"> <Box sx={{ width: 120, height: 40, bgcolor: `grey.${shade}`, borderRadius: 1, border: '1px solid', borderColor: 'divider', }} /> <Typography variant="body2" sx={{ minWidth: 32 }}> {shade} </Typography> </Stack> ))} </Stack> ); }
タイポグラフィ
タイポグラフィのバリアント(h1–h6、subtitle1/subtitle2、body1/body2、caption、overline、button)はテーマスケールから取得されます。ボタンは typography.button から textTransform: 'none' も継承します。
function Example() { const variants = [ { label: '見出し 1', variant: 'h1' }, { label: '見出し 2', variant: 'h2' }, { label: '見出し 3', variant: 'h3' }, { label: '見出し 4', variant: 'h4' }, { label: '見出し 5', variant: 'h5' }, { label: '見出し 6', variant: 'h6' }, { label: 'サブタイトル 1', variant: 'subtitle1' }, { label: 'サブタイトル 2', variant: 'subtitle2' }, { label: '本文 1 — 通常の段落テキスト', variant: 'body1' }, { label: '本文 2 — 小さめの段落テキスト', variant: 'body2' }, { label: 'キャプションテキスト', variant: 'caption' }, { label: 'オーバーラインテキスト', variant: 'overline' }, ]; return ( <Stack spacing={2} sx={{ p: 3 }}> {variants.map(({ label, variant }) => ( <Typography key={variant} variant={variant}> {label} </Typography> ))} </Stack> ); }
ボタン(MuiButton、MuiButtonBase)
塗りつぶし、アウトライン、テキストの各バリアント。サイズ、セマンティックカラー、無効状態。
function Example() { return ( <Stack spacing={2} sx={{ p: 3 }}> <Stack direction="row" spacing={2} sx={{ flexWrap: 'wrap', alignItems: 'center' }}> <Button variant="contained" size="small">小 塗りつぶし</Button> <Button variant="contained" size="medium">中 塗りつぶし</Button> <Button variant="contained" size="large">大 塗りつぶし</Button> </Stack> <Stack direction="row" spacing={2} sx={{ flexWrap: 'wrap', alignItems: 'center' }}> <Button variant="outlined" size="small">小 アウトライン</Button> <Button variant="outlined" size="medium">中 アウトライン</Button> <Button variant="outlined" size="large">大 アウトライン</Button> </Stack> <Stack direction="row" spacing={2} sx={{ flexWrap: 'wrap', alignItems: 'center' }}> <Button variant="text" size="small">小 テキスト</Button> <Button variant="text" size="medium">中 テキスト</Button> <Button variant="text" size="large">大 テキスト</Button> </Stack> <Stack direction="row" spacing={2} sx={{ flexWrap: 'wrap', alignItems: 'center' }}> <Button variant="contained" color="primary">プライマリ</Button> <Button variant="contained" color="secondary">セカンダリ</Button> <Button variant="contained" color="error">エラー</Button> <Button variant="contained" color="warning">警告</Button> <Button variant="contained" color="info">情報</Button> <Button variant="contained" color="success">成功</Button> </Stack> <Stack direction="row" spacing={2} sx={{ flexWrap: 'wrap' }}> <Button variant="contained" disabled>無効</Button> <Button variant="outlined" disabled>無効(アウトライン)</Button> </Stack> </Stack> ); }
テキストフィールド(MuiOutlinedInput、MuiInputLabel、MuiFormLabel、MuiFormHelperText)
ラベルはフィールドの上に配置されます(太字、フローティングなし)。アウトライン入力は角丸と primary のフォーカスリングを使用します。
function Example() { return ( <Stack spacing={2} sx={{ p: 3, maxWidth: 400 }}> <TextField label="標準" variant="outlined" /> <TextField label="値あり" variant="outlined" defaultValue="サンプルテキスト" /> <TextField label="無効" variant="outlined" disabled /> <TextField label="必須" variant="outlined" required /> <TextField label="ヘルパーテキスト付き" variant="outlined" helperText="重要なヘルパーテキスト" /> <TextField label="エラー状態" variant="outlined" error helperText="エラーメッセージ" /> </Stack> ); }
セレクト(MuiSelect)
セレクトはテーマのアウトライン入力スタイルを継承します。
function Example() { const [value, setValue] = React.useState(''); return ( <Stack spacing={2} sx={{ p: 3, maxWidth: 300 }}> <FormControl fullWidth> <InputLabel id="theme-select-label">オプション</InputLabel> <Select labelId="theme-select-label" value={value} label="オプション" onChange={(e) => setValue(e.target.value)} > <MenuItem value="option1">オプション 1</MenuItem> <MenuItem value="option2">オプション 2</MenuItem> <MenuItem value="option3">オプション 3</MenuItem> </Select> </FormControl> <FormControl fullWidth error> <InputLabel id="theme-select-error">エラー状態</InputLabel> <Select labelId="theme-select-error" value="" label="エラー状態"> <MenuItem value="option1">オプション 1</MenuItem> </Select> </FormControl> <FormControl fullWidth disabled> <InputLabel id="theme-select-disabled">無効</InputLabel> <Select labelId="theme-select-disabled" value="" label="無効"> <MenuItem value="option1">オプション 1</MenuItem> </Select> </FormControl> </Stack> ); }
日付ピッカー入力(MuiPickersOutlinedInput)
日付ピッカーはテキストフィールドと同じアウトライン入力のオーバーライドを使用します。
function Example() { return ( <LocalizationProvider dateAdapter={AdapterLuxon}> <Box sx={{ p: 3, maxWidth: 300 }}> <DatePicker label="開始日" /> </Box> </LocalizationProvider> ); }
テーブル(MuiTable、MuiTableCell、MuiTableHead、MuiTableBody、MuiTableContainer、MuiTableRow)
全幅レイアウト、区切り線のボーダー、ヘッダーサイズ、grey.50 上の行ホバー。
function Example() { return ( <TableContainer component={Paper} sx={{ m: 3 }}> <Table> <TableHead> <TableRow> <TableCell>名前</TableCell> <TableCell>メール</TableCell> <TableCell>役割</TableCell> <TableCell>ステータス</TableCell> </TableRow> </TableHead> <TableBody> <TableRow> <TableCell>山田 太郎</TableCell> <TableCell>taro@example.com</TableCell> <TableCell>管理者</TableCell> <TableCell><Chip label="アクティブ" color="success" size="small" /></TableCell> </TableRow> <TableRow> <TableCell>佐藤 花子</TableCell> <TableCell>hanako@example.com</TableCell> <TableCell>ユーザー</TableCell> <TableCell><Chip label="保留中" color="warning" size="small" /></TableCell> </TableRow> <TableRow> <TableCell>鈴木 一郎</TableCell> <TableCell>ichiro@example.com</TableCell> <TableCell>ユーザー</TableCell> <TableCell><Chip label="アクティブ" color="success" size="small" /></TableCell> </TableRow> </TableBody> </Table> </TableContainer> ); }
タブ(MuiTab)
ナビゲーション用のタブストリップスタイル。
function Example() { const [tab, setTab] = React.useState(0); return ( <Box sx={{ p: 3 }}> <Tabs value={tab} onChange={(_, v) => setTab(v)}> <Tab label="タブ 1" /> <Tab label="タブ 2" /> <Tab label="タブ 3" /> </Tabs> <Typography sx={{ mt: 2 }}>タブ {tab + 1} のコンテンツ</Typography> </Box> ); }
メニュー(MuiMenu、MuiMenuItem、MuiListItemIcon)
アイコン配置付きのドロップダウンメニュー。
function Example() { const [anchorEl, setAnchorEl] = React.useState(null); const open = Boolean(anchorEl); return ( <Box sx={{ p: 3 }}> <Button onClick={(e) => setAnchorEl(e.currentTarget)}>メニューを開く</Button> <Menu anchorEl={anchorEl} open={open} onClose={() => setAnchorEl(null)}> <MenuItem onClick={() => setAnchorEl(null)}> <ListItemIcon><Home fontSize="small" /></ListItemIcon> ホーム </MenuItem> <MenuItem onClick={() => setAnchorEl(null)}> <ListItemIcon><Person fontSize="small" /></ListItemIcon> プロフィール </MenuItem> <MenuItem onClick={() => setAnchorEl(null)}> <ListItemIcon><Settings fontSize="small" /></ListItemIcon> 設定 </MenuItem> <Divider /> <MenuItem onClick={() => setAnchorEl(null)}> <ListItemIcon><Logout fontSize="small" /></ListItemIcon> ログアウト </MenuItem> </Menu> </Box> ); }
リンク(MuiLink)
palette.link と GlobalStyles のグローバルアンカースタイルを使用します。
function Example() { return ( <Stack spacing={2} sx={{ p: 3 }}> <Stack direction="row" spacing={3} alignItems="center" sx={{ flexWrap: 'wrap' }}> <Link href="#">デフォルトリンク</Link> <Link href="#" sx={{ color: 'secondary.main' }}>セカンダリリンク</Link> <Link href="#" sx={{ color: 'error.main' }}>エラーリンク</Link> </Stack> <Stack direction="row" spacing={3} alignItems="center" sx={{ flexWrap: 'wrap' }}> <Link href="#">下線なし</Link> <Link href="#" underline="hover">ホバー時に下線</Link> <Link href="#" underline="always">常に下線</Link> </Stack> <Typography variant="body1"> テキスト内の<Link href="#">インラインリンク</Link>を含む段落。 </Typography> </Stack> ); }
ページネーション(MuiPagination、MuiPaginationItem)
primary、secondary、アウトラインの各バリアントを持つページコントロール。
function Example() { const [page, setPage] = React.useState(1); return ( <Stack spacing={2} sx={{ p: 3 }}> <Pagination count={10} page={page} onChange={(_, v) => setPage(v)} color="primary" /> <Pagination count={10} color="secondary" /> <Pagination count={10} variant="outlined" color="primary" shape="rounded" /> </Stack> ); }
円形プログレス(MuiCircularProgress)
セマンティックカラー別のローディングインジケーター。
function Example() { return ( <Stack direction="row" spacing={4} alignItems="center" sx={{ p: 3, flexWrap: 'wrap' }}> <Box textAlign="center"> <CircularProgress size={40} /> <Typography variant="caption" display="block" sx={{ mt: 1 }}>デフォルト</Typography> </Box> <Box textAlign="center"> <CircularProgress size={40} color="secondary" /> <Typography variant="caption" display="block" sx={{ mt: 1 }}>セカンダリ</Typography> </Box> <Box textAlign="center"> <CircularProgress size={40} color="success" /> <Typography variant="caption" display="block" sx={{ mt: 1 }}>成功</Typography> </Box> <Box textAlign="center"> <CircularProgress size={40} color="error" /> <Typography variant="caption" display="block" sx={{ mt: 1 }}>エラー</Typography> </Box> </Stack> ); }
Paper とカード(MuiPaper、MuiCard)
レイアウトセクションとコンテンツカード用のサーフェス。
function Example() { return ( <Stack direction="row" spacing={2} sx={{ p: 3, flexWrap: 'wrap', gap: 2 }}> <Card sx={{ minWidth: 200 }}> <CardContent> <Typography variant="h6" gutterBottom>カードタイトル</Typography> <Typography variant="body2" color="text.secondary"> テーマ適用済みサーフェスのカードコンポーネント。 </Typography> </CardContent> </Card> <Card variant="outlined" sx={{ minWidth: 200 }}> <CardContent> <Typography variant="h6" gutterBottom>アウトラインカード</Typography> <Typography variant="body2" color="text.secondary"> アウトラインのカードバリアント。 </Typography> </CardContent> </Card> <Paper sx={{ p: 2, minWidth: 200 }}> <Typography variant="h6" gutterBottom>Paper コンポーネント</Typography> <Typography variant="body2" color="text.secondary"> グループ化されたコンテンツ用の Paper サーフェス。 </Typography> </Paper> </Stack> ); }
区切り線(MuiDivider)
palette.divider を使った水平・垂直・テキスト配置の区切り線。
function Example() { return ( <Stack spacing={2} sx={{ p: 3, maxWidth: 400 }}> <Box sx={{ bgcolor: 'background.paper', p: 2 }}> <Typography>上のコンテンツ</Typography> <Divider sx={{ my: 2 }} /> <Typography>下のコンテンツ</Typography> </Box> <Divider>中央</Divider> <Divider textAlign="left">左</Divider> <Divider textAlign="right">右</Divider> <Stack direction="row" spacing={2} sx={{ height: 40, alignItems: 'center' }}> <Typography>項目 1</Typography> <Divider orientation="vertical" flexItem /> <Typography>項目 2</Typography> <Divider orientation="vertical" flexItem /> <Typography>項目 3</Typography> </Stack> </Stack> ); }