🎨

【MUI】StackでFlexboxのgapが使えるようになった(実験的機能)

2023/04/14に公開

はじめに

MUI v5.11.15 で Stack に useFlexGap prop が追加されました。
https://github.com/mui/material-ui/releases/tag/v5.11.15
これまでは Stack の spacing を指定すると margin が設定されていましたが、useFlexGap を有効にすることで Flexbox の gap が設定されるようになります🎉

環境

  • @mui/material v5.11.16

デモで使用しているコードは下記の StackBlitz 上で試せるようにしています。もし、挙動を確認したい方がいればこちらも合わせてご確認ください。
https://stackblitz.com/edit/demo-use-flex-gap

前提

Stack 自体の説明は省きます🙏
Stack について知りたい方は下記を見ていただければと思います。
https://mui.com/material-ui/react-stack/

これまでの spacing の挙動

spacing を指定することで、子要素の間隔を設定できます。これまでは margin が使われていましたが、全ての子要素に margin をつけると無駄な余白ができてしまうので、下記のような一工夫が入っていました。(横並びの場合は marginLeft)

& > :not(style) + :not(style)': { marginTop: 16px }

簡単に説明すると、Stack 直下の子要素以外に対して margin を設定しています。つまり、要素の位置に依存する形で margin が適用されます。

margin による指定の問題点

order で子要素を並び替えるとデザインが崩れる

Stack 内の要素は order を指定することで任意の並びにできます。ただし、order で変更できるのは見た目上の並びで、ソース内の順序は変わりません。
その影響で上述した margin の適用時は order の指定が考慮されません。
試しに order を指定した時の挙動を確認してみましょう。

まずは order なしのパターンを見てみます。

import { Stack, Typography, TextField, Divider } from "@mui/material";

function App() {
  return (
    <Stack alignItems="center" spacing={4}>
      <Typography variant="h5">これまで通り(margin)</Typography>
      <Stack spacing={6}>
        <Typography>テスト1</Typography>
        <Typography>テスト2</Typography>
        <Typography>テスト3</Typography>
      </Stack>
    </Stack>
  );
}

export default App;

間隔が設定されているので良さそうです。

次に order を指定して並びを変えてみます。

import { Stack, Typography, TextField, Divider } from "@mui/material";

function App() {
  return (
    <Stack alignItems="center" spacing={4}>
      <Typography variant="h5">これまで通り(margin)</Typography>
      <Stack spacing={6}>
+       <Typography order={3}>テスト1</Typography>
+       <Typography order={1}>テスト2</Typography>
+       <Typography order={2}>テスト3</Typography>
      </Stack>
    </Stack>
  );
}

export default App;

すると、テスト2 の上に不要な margin が設定されたのと、 テスト3・テスト1 の間隔が設定されなくなってしまいました。

orderによる要素の並び替え後

Devtools で確認してみると、テスト2 に対しても margin が適用されていることが分かります。このように margin だと order で子要素を並び替えた時にデザインが崩れてしまいます。

参考
https://github.com/mui/material-ui/issues/33810

Stack で横並びにして子要素の間隔を詰めるとずれてしまう

Stack の direction="row" を指定して子要素を横並びにし、子要素として TextField を2つ並べてみます。また、TextField は margin="dense" を指定することで詰めて並べることができます。

import { Stack, Typography, TextField, Divider } from "@mui/material";

function App() {
  return (
    <Stack alignItems="center" spacing={4}>
      <Typography variant="h5">これまで通り(margin)</Typography>
      <Stack spacing={6}>
        <Typography order={3}>テスト1</Typography>
        <Typography order={1}>テスト2</Typography>
        <Typography order={2}>テスト3</Typography>
      </Stack>

+     <Stack spacing={6} direction="row">
+       <TextField margin="dense" />
+       <TextField margin="dense" />
+     </Stack>
+     <Stack spacing={6} direction="row">
+       <TextField margin="dense" />
+       <TextField margin="dense" />
+     </Stack>
    </Stack>
  );
}

export default App;

すると、下記のようにテキストボックスが少しずれてしまいます。この現象についてはなぜ起きるのか把握できていません...

Stackで横並びにして上でTextFieldの余白を詰めた時

参考
https://github.com/mui/material-ui/issues/33155

Flexbox の gap を試してみる

margin で指定した時の問題点がわかったので、次は Flexbox の gap を使用して問題が解消されるか確認してみます。...と言いたいところですが、現時点では Flexbox の gap を適用する方法が2種類あるので、先に適用方法を簡単に確認しておきます。

Flexbox の gap を適用する方法

Stack ごとに Flexbox の gap を適用する

まずは、Stack に useFlexGap を指定する方法です。こちらは Stack ごとに変えられるので柔軟な設定が可能です。

<Stack spacing={6} useFlexGap>
  <Typography order={3}>テスト1</Typography>
  <Typography order={1}>テスト2</Typography>
  <Typography order={2}>テスト3</Typography>
</Stack>

デフォルトを Flexbox の gap にする

これは useFlexGap に限らないですが、ThemeProvider を使うことで、アプリケーション全体の設定を変更できます。ここで MuiStack の useFlexGap を指定すると、デフォルトで Flexbox の gap が適用されます。

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
+ import { createTheme, ThemeProvider } from '@mui/material'

+ const customThema = createTheme({
+   components: {
+     MuiStack: {
+       defaultProps: {
+         useFlexGap: true,
+       }
+     }
+   }
+ })

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <React.StrictMode>
+    <ThemeProvider theme={customThema}>
      <App />
+    </ThemeProvider>
  </React.StrictMode>,
)

個人的には、今後の手間を考えると ThemeProvider の useFlexGap を指定してデフォルトを変えるのが良さそうかなと考えています。理由は下記のコメントを見る限り、v6 から Flexbox の gap がデフォルトになりそうだからです。
https://github.com/mui/material-ui/issues/33754#issuecomment-1221532573

Flexbox の gap がデフォルトになれば、useFlexGap の指定は不要になります。そうなると、useFlexGap を消したい!となりそうですが、Stack ごとに useFlexGap を指定していた場合は削除してまわる必要があります。一方、全体で適用している場合は1箇所修正すれば済むはずです。

useFlexGap を試してみる

margin による指定の問題点で使用したコードを複製して useFlexGap を適用してみます。

import { Stack, Typography, TextField, Divider } from "@mui/material";

function App() {
  return (
    <Stack alignItems="center" spacing={4}>
      <Typography variant="h5">これまで通り(margin)</Typography>
      <Stack spacing={6}>
        <Typography order={3}>テスト1</Typography>
        <Typography order={1}>テスト2</Typography>
        <Typography order={2}>テスト3</Typography>
      </Stack>

      <Stack spacing={6} direction="row">
        <TextField margin="dense" />
        <TextField margin="dense" />
      </Stack>
      <Stack spacing={6} direction="row">
        <TextField margin="dense" />
        <TextField margin="dense" />
      </Stack>

+      <Typography variant="h5">Flexbox の gap適用</Typography>
+      <Stack spacing={6} useFlexGap>
+        <Typography order={3}>テスト1</Typography>
+        <Typography order={1}>テスト2</Typography>
+        <Typography order={2}>テスト3</Typography>
+      </Stack>
+
+      <Stack spacing={6} direction="row" useFlexGap>
+        <TextField margin="dense" />
+        <TextField margin="dense" />
+      </Stack>
+      <Stack spacing={6} direction="row" useFlexGap>
+        <TextField margin="dense" />
+        <TextField margin="dense" />
+      </Stack>
    </Stack>
  );
}

export default App;

下記が実行した結果ですが、どちらの問題も解消されていますね🎉

useFlexGapを適用

Devtools で確認すると、確かに Flexbox に gap が適用されています。

おわりに

まだ MUI を使っているプロダクトでは試せてないので、そのうち試してみようかなと思っています💪(まずはバージョンを上げなくては...)

参考

Discussion