まくろぐ

Material-UI のコンポーネントに独自の CSS スタイルを設定する (makeStyle)

更新:
作成:

Material-UI コンポーネントのスタイル設定

Material-UI が提供する各種コンポーネントには、表示スタイルを切り替えるためのプロパティが用意されています。 例えば、Button コンポーネントvariantcolor プロパティで見た目を切り替えることができます。

<Button>Default</Button>
<Button variant="contained" color="primary">Primary</Button>
<Button variant="contained" color="secondary">Secondary</Button>
<Button variant="outlined" disabled>Disabled</Button>

多くのケースでは、この仕組みで十分にスタイル設定できるのですが、デフォルトのスタイルから外れた表示をしたり、div 要素など Material-UI 以外のコンポーネントに対して独自の CSS を適用したいことがあります。 このような場合、コンポーネントの実装ファイル内に直接 CSS コードを記述してスタイルをカスタマイズできます(JavaScript 内に記述するので CSS-in-JS と呼びます)。 React の世界では色々な CSS 参照方法がありますが、Material-UI は次のような理由で CSS-in-JS な記述方法を採用しています。

  • 現在のテーマ設定に基づいたスタイル設定を行える(例: 基準スペースの2倍のマージンを設定する)
  • コンポーネントの props の値を使って動的にスタイル設定できる(例: <MyButton color="vivid"> で派手な色のスタイルを設定する)

フックによるスタイル設定 (makeStyle)

Material-UI で、コンポーネントに独自スタイルを設定する方法としては、主に次の 3 種類の方法が用意されています。

  • Hook API
    • makeStyle 関数で生成したフック関数をコンポーネント内で呼び出す方法。一番よく使われてる。
  • Styled components API
    • 既存のコンポーネント (Button など)をラップする形で、スタイルを適用したコンポーネント(MyButton など)を作成する方法。
  • Higher-order component API
    • Styled components に似てるけど、HoC の仕組みでスタイル設定したコンポーネントを作成する方法。ちょっとわかりにくい。

ここでは、一番メジャーで分かりやすい、フックを利用したスタイル設定方法を紹介します。 次の例では、Material-UI の Button コンポーネントに、独自の CSS スタイル(customButton クラス)を適用しています。

/p/cw9ju6f/img-001.png
components/MyButton.tsx
import { Button } from '@material-ui/core'
import { makeStyles } from '@material-ui/core/styles'

const useStyles = makeStyles({
  customButton: {
    color: 'white',
    background: '#229966',
    padding: '1em 3em',
  },
})

const MyButton: React.FC = () => {
  const classes = useStyles()
  return <Button className={classes.customButton}>Button</Button>
}

export default MyButton

ポイントは、makeStyles 関数 で作成したフック関数を呼び出して、その戻り値のオブジェクトを使って各コンポーネントの className プロパティを指定するところです。 基本はこれだけなので、このやり方に慣れてしまえば OK です。

Props によるスタイル設定の分岐

CSS-in-JS なスタイル記述方法の利点の一つとして、次のような動的なスタイル設定があります。 この例では、スタイル設定用のフック関数 (useStyles) に、StyleProps 型の引数を渡すことでテキストの色を切り替えています。

components/MyLabel.tsx
import { Typography } from '@material-ui/core'
import { makeStyles, Theme } from '@material-ui/core/styles'

// スタイル定義用のフック関数が受け取るプロパティの型
type StyleProps = {
  textStyle: 'normal' | 'vivid'
}

const useStyles = makeStyles<Theme, StyleProps>({
  customText: {
    // 引数で渡されたオブジェクトの値で分岐処理できる
    color: (props) => (props.textStyle === 'normal' ? 'black' : '#ff6633'),
  },
})

const MyLabel: React.FC = () => {
  const classes = useStyles({ textStyle: 'vivid' })
  return <Typography className={classes.customText}>Hello</Typography>
}

export default MyLabel

上記の例では、コンポーネントの実装内で StyleProps オブジェクトを作成して useStyles に渡していますが、コンポーネント自身の Props オブジェクトをそのまま渡してしまう方が一般的かもしれません。

components/MyLabel.tsx
import { Typography } from '@material-ui/core'
import { makeStyles, Theme } from '@material-ui/core/styles'

type Props = {
  textStyle?: 'normal' | 'vivid'
  children: React.ReactNode
}

const useStyles = makeStyles<Theme, Props>({
  customText: {
    color: (props) => (props.textStyle === 'normal' ? 'black' : '#ff6633'),
  },
})

const MyLabel: React.FC<Props> = (props: Props) => {
  const classes = useStyles(props)
  return (
    <Typography className={classes.customText}>{props.children}</Typography>
  )
}

MyLabel.defaultProps = {
  textStyle: 'normal',
}

export default MyLabel

こうすると、そのコンポーネントを使う側でスタイルを制御できるようになります。

/p/cw9ju6f/img-002.png
pages/index.tsx
import MyLabel from '../components/MyLabel'

const IndexPage: React.FC = () => {
  return (
    <div>
      <MyLabel>Default Label</MyLabel>
      <MyLabel textStyle="normal">Normal Label</MyLabel>
      <MyLabel textStyle="vivid">Vivid Label</MyLabel>
    </div>
  )
}

export default IndexPage

オブジェクト間のマージン用にスタイル設定する

makeStyle で作成したスタイル設定は、Material-UI のコンポーネント以外にも適用できます。 次の例では、div 要素に独自スタイルを適用し、子要素のマージンを現在のテーマのスペース 1 つ分 (theme.spacing(1)) に設定しています。 スタイルオブジェクトを入れ子の形で定義すると、プロパティ名の部分で & を使ってカレント要素を参照できます。

/p/cw9ju6f/img-003.png
import { Button } from '@material-ui/core'
import { makeStyles, Theme } from '@material-ui/core/styles'

const useStyles = makeStyles((theme: Theme) => ({
  root: {
    '& > *': {
      margin: theme.spacing(1),
    },
  },
}))

const IndexPage: React.FC = () => {
  const classes = useStyles()

  return (
    <div className={classes.root}>
      <Button variant="contained">Button 1</Button>
      <Button variant="contained">Button 2</Button>
      <Button variant="contained">Button 3</Button>
    </div>
  )
}

export default IndexPage

関連記事

まくろぐ
サイトマップまくへのメッセージ