メインコンテンツまでスキップ

テーマ

@nodeblocks/frontend-theme は、Nodeblocks のフロントエンドブロックで使われる共有の Material UI デザインシステムです。事前設定済みの defaultTheme、グローバルスタイルと CSS ベースラインを登録するルートの ThemeProvider、およびアプリのブランディング時にマージできるトークンモジュール(palettetypographyspacingcomponents)を提供します。

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: defaultThemeThemeProvideruseThemeGlobalStylespalettetypographyspacingcomponentsThemeProviderProps


テーマ適用コンポーネント

ライブ例では ThemeProvider を省略

このページの tsx live スニペットはすべて、内部で defaultThemeThemeProvider でプレビューをラップするドキュメント Playground 内でレンダリングされます。そのため例ではラッパーを省略し、UI を読みやすくしています。独自のアプリケーションでは、使い方 で示したとおり、ルートに ThemeProvider をマウントする必要があります。

以下のセクションは、blocks/frontend-theme の Storybook プレビュー(SharedTheme.stories.tsx)に対応しています。各スニペットは components.ts でテーマオーバーライドがある MUI コンポーネントのスタイル出力を示す tsx live 例です。

カラーパレット

パレットトークンは primarysecondary、セマンティックカラー、背景、テキストを制御します。

ライブエディター
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>
  );
}
結果
Loading...

グレースケール

拡張された grey の段階(257550900)がパレットで定義されています。

ライブエディター
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>
  );
}
結果
Loading...

タイポグラフィ

タイポグラフィのバリアント(h1h6subtitle1/subtitle2body1/body2captionoverlinebutton)はテーマスケールから取得されます。ボタンは 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>
  );
}
結果
Loading...

ボタン(MuiButtonMuiButtonBase

塗りつぶし、アウトライン、テキストの各バリアント。サイズ、セマンティックカラー、無効状態。

ライブエディター
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>
  );
}
結果
Loading...

テキストフィールド(MuiOutlinedInputMuiInputLabelMuiFormLabelMuiFormHelperText

ラベルはフィールドの上に配置されます(太字、フローティングなし)。アウトライン入力は角丸と 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>
  );
}
結果
Loading...

セレクト(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>
  );
}
結果
Loading...

日付ピッカー入力(MuiPickersOutlinedInput

日付ピッカーはテキストフィールドと同じアウトライン入力のオーバーライドを使用します。

ライブエディター
function Example() {
  return (
    <LocalizationProvider dateAdapter={AdapterLuxon}>
      <Box sx={{ p: 3, maxWidth: 300 }}>
        <DatePicker label="開始日" />
      </Box>
    </LocalizationProvider>
  );
}
結果
Loading...

テーブル(MuiTableMuiTableCellMuiTableHeadMuiTableBodyMuiTableContainerMuiTableRow

全幅レイアウト、区切り線のボーダー、ヘッダーサイズ、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>
  );
}
結果
Loading...

タブ(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>
  );
}
結果
Loading...

メニュー(MuiMenuMuiMenuItemMuiListItemIcon

アイコン配置付きのドロップダウンメニュー。

ライブエディター
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>
  );
}
結果
Loading...

palette.linkGlobalStyles のグローバルアンカースタイルを使用します。

ライブエディター
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>
  );
}
結果
Loading...

ページネーション(MuiPaginationMuiPaginationItem

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>
  );
}
結果
Loading...

円形プログレス(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>
  );
}
結果
Loading...

Paper とカード(MuiPaperMuiCard

レイアウトセクションとコンテンツカード用のサーフェス。

ライブエディター
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>
  );
}
結果
Loading...

区切り線(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>
  );
}
結果
Loading...