MUI の sx とは?よく見るけど結局何なのかを一記事で理解する

目次

sx プロップの正体

MUIのコードを読んでいると、ほぼすべてのコンポーネントにsxというプロップが付いている。これはMUIが提供するインラインスタイリングの仕組みで、CSSプロパティをオブジェクト形式で直接コンポーネントに渡せる。

<Box sx={{ padding: 2, backgroundColor: 'primary.main', color: 'white' }}>
  Hello
</Box>

見た目はReactのstyleプロップに似ているが、中身はまったく別物だ。styleはブラウザのインラインスタイルをそのまま出力するだけだが、sxはMUIのテーマシステムと統合されており、レスポンシブ対応やテーマトークンの参照といった機能を持っている。

要するに、CSSファイルを別に書かなくても、コンポーネントに直接スタイルを宣言できる仕組みであり、MUIが内部で使っているEmotionというCSS-in-JSライブラリを通じて実際のCSSクラスに変換される。インラインスタイルのように見えるが、出力されるのはちゃんとしたCSSだ。

style プロップとの決定的な違い

sxstyleを混同している人は多いが、できることの幅がまるで違う。以下に主要な違いを整理する。

機能style プロップsx プロップ
テーマの値を参照不可'primary.main'等で参照可
レスポンシブ指定不可ブレークポイント指定が可能
擬似クラス(:hover等)不可'&:hover': { ... } で指定可
ショートハンド(m, p等)不可m: 2margin: 16px
ネストセレクタ不可'& .child': { ... } で指定可
出力形式インラインstyle属性CSSクラス

styleプロップではstyle={{ color: theme.palette.primary.main }}のようにテーマオブジェクトを手動で参照する必要がある。sxならsx={{ color: 'primary.main' }}と書くだけでテーマの値を引ける。この差は、特にテーマを切り替えるアプリケーションで大きい。

テーマとの連携

sxの最大の強みは、MUIのテーマシステムとシームレスにつながっている点だ。テーマに定義された値を文字列で参照できる。

<Box
  sx={{
    color: 'text.secondary',          // theme.palette.text.secondary
    backgroundColor: 'background.paper', // theme.palette.background.paper
    borderRadius: 1,                   // theme.shape.borderRadius * 1
    boxShadow: 3,                      // theme.shadows[3]
  }}
/>

どのCSSプロパティがテーマのどこを参照するかは、MUIのデザイントークンのマッピングルールで決まっている。以下がそのマッピングの概要だ。

sx プロパティ → テーマトークン マッピング sx プロパティ color, bgcolor p, m, mx, py … fontSize borderRadius boxShadow zIndex テーマの参照先 theme.palette theme.spacing theme.typography theme.shape theme.shadows theme.zIndex 数値を渡すと、対応するテーマトークンの値に自動変換される

たとえばp: 2と書くと、MUIはテーマのspacing関数(デフォルトで8px * n)を使ってpadding: 16pxに変換する。テーマをカスタマイズしていれば、その値が反映される。これがテーマ連携の実体だ。

スペーシングのショートハンド

sxではCSSのmarginpaddingを短縮記法で書ける。慣れると非常に効率がよい。

ショートハンド対応するCSSプロパティ
mmargin
mt, mr, mb, mlmargin-top, -right, -bottom, -left
mxmargin-left + margin-right
mymargin-top + margin-bottom
ppadding
pt, pr, pb, plpadding-top, -right, -bottom, -left
pxpadding-left + padding-right
pypadding-top + padding-bottom
<Box sx={{ mx: 'auto', py: 3, px: { xs: 2, md: 4 } }}>
  レスポンシブなコンテナ
</Box>

数値はテーマのspacing倍数として扱われる(デフォルトで1 = 8px)。'20px'のように文字列で渡せば、生のCSS値としてそのまま使える。

レスポンシブ対応の書き方

sxでは、プロパティの値にブレークポイントごとのオブジェクトを渡すだけでレスポンシブ対応ができる。メディアクエリを自分で書く必要はない。

<Box
  sx={{
    width: {
      xs: '100%',  // 0px以上
      sm: '80%',   // 600px以上
      md: '60%',   // 900px以上
      lg: '50%',   // 1200px以上
    },
    flexDirection: {
      xs: 'column',
      md: 'row',
    },
  }}
/>

MUIのデフォルトブレークポイントはxs: 0, sm: 600, md: 900, lg: 1200, xl: 1536(単位px)だ。これらはテーマのbreakpointsでカスタマイズできる。

配列構文も使える。配列のインデックスがブレークポイントの順序に対応する。

<Box sx={{ width: ['100%', '80%', '60%', '50%'] }} />
// xs: 100%, sm: 80%, md: 60%, lg: 50%

ただし、配列構文は途中のブレークポイントを飛ばすときにnullを入れる必要があるため、オブジェクト構文のほうが可読性は高い。チームで使う場合はオブジェクト構文に統一することを推奨する。

擬似クラスとネストセレクタ

sxでは、&を使ったCSSセレクタのネストが可能だ。これはstyleプロップにはない機能であり、sxを使う大きな動機の一つでもある。

<Button
  sx={{
    '&:hover': {
      backgroundColor: 'primary.dark',
      transform: 'scale(1.02)',
    },
    '&:disabled': {
      opacity: 0.5,
    },
    '& .MuiButton-startIcon': {
      marginRight: 1,
    },
  }}
>
  ボタン
</Button>

&はそのコンポーネント自身を指す。SCSSやstyled-componentsでの&と同じ意味だ。子要素のスタイルを上書きしたいときにも、'& .className'の形式で書ける。

MUIのコンポーネントは内部にスロット用のCSSクラス(MuiButton-startIconなど)を持っているため、sxのネストセレクタでそれらのスタイルを細かく制御できる。

コールバック形式でテーマを直接参照する

sxの値には関数を渡すこともできる。引数としてテーマオブジェクトを受け取るため、条件分岐や計算を含むスタイルを定義したい場合に便利だ。

<Box
  sx={(theme) => ({
    padding: theme.spacing(2),
    backgroundColor: theme.palette.mode === 'dark'
      ? theme.palette.grey[800]
      : theme.palette.grey[100],
    border: `1px solid ${theme.palette.divider}`,
  })}
/>

文字列ショートハンドでは表現しきれないケース、たとえばテンプレートリテラルでborderを組み立てたいときや、ダークモードかどうかで分岐したいときに使う。ただし、大半のケースでは文字列ショートハンドで足りるので、コールバック形式は必要なときだけ使えばよい。

styled() との使い分け

MUIでのスタイリングには、sxプロップのほかにstyled()ユーティリティがある。どちらもCSS-in-JSでスタイルを適用する手段だが、使いどころが異なる。

判断の基軸はそのスタイルを再利用するかどうかだ。

sxが向いているケースは、特定の画面やレイアウトでの一度きりの微調整、プロトタイピング時の素早いスタイル適用、条件分岐で動的にスタイルを変えたい場合など。コンポーネントの宣言箇所にスタイルがインラインで見えるため、スタイルの把握が早い。

styled()が向いているケースは、プロジェクト全体で繰り返し使うカスタムコンポーネントの定義、複雑なスタイルロジックの分離、デザインシステムの部品として共有する場合など。スタイルがコンポーネント定義と一体になるため、使う側はスタイルの詳細を意識しなくてよい。

// styled() で再利用可能なコンポーネントを定義
const Card = styled(Box)(({ theme }) => ({
  padding: theme.spacing(3),
  borderRadius: theme.shape.borderRadius * 2,
  boxShadow: theme.shadows[2],
}));

// sx で一度きりの微調整をする
<Card sx={{ mt: 4, maxWidth: 480 }}>
  コンテンツ
</Card>

このように、styled()で定義したコンポーネントに対して、さらにsxで微調整するという組み合わせが実務では自然な形になる。どちらか一方に統一するよりも、役割で使い分けるのが実用的だ。

パフォーマンス上の注意点

sxはレンダリングのたびにスタイルオブジェクトを解析してCSSに変換する。そのため、大量のコンポーネントにsxを適用している場合はパフォーマンスに影響する可能性がある。

意識すべきポイントは2つある。

1つ目は、sxに渡すオブジェクトをコンポーネントの外で定数として定義すること。レンダリングごとに新しいオブジェクトが生成されると、不要な再計算が発生する。

// オブジェクトを外に出す
const cardStyles = {
  p: 2,
  borderRadius: 2,
  boxShadow: 1,
};

function MyCard() {
  return <Box sx={cardStyles}>コンテンツ</Box>;
}

2つ目は、リスト内の数百〜数千のアイテムすべてにsxを使うような場面では、styled()への切り替えを検討すること。styled()はコンポーネント定義時に一度だけスタイルを生成するため、繰り返しレンダリングされる要素には有利だ。

とはいえ、通常のアプリケーションで数十個のsxを使っている程度では体感できる差はほぼない。最適化は計測してボトルネックが見えてからで十分だ。

実践のヒント:sx を使いこなすために

sxの基本から応用まで一通り見てきた。ここからは、実際のプロジェクトでsxを運用する際に役立つポイントを挙げる。

まず、テーマの設計を先にやること。sxはテーマの値を引く仕組みなので、テーマ側にカラーパレット、スペーシング、タイポグラフィが整理されていないと、結局マジックナンバーの羅列になってしまう。sxの恩恵を最大化するには、createTheme()でプロジェクト固有のトークンを定義しておくのが前提になる。

次に、チーム内でsxとstyled()の使い分けルールを決めること。判断基準は先述のとおり再利用性が軸だが、プロジェクトによっては「3回以上使うならstyled()に切り出す」のような具体的な基準を設けたほうが運用しやすい。

最後に、MUIの公式ドキュメントにあるsx prop のページは、対応するテーマキーの全一覧が載っている。ショートハンドやテーママッピングで迷ったときはここを引くのが最速だ。ブックマークしておいて損はない。

目次