まくろぐ

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

関連記事

Next.js で Material-UI を使う

更新:
作成:
/p/s6djqw3/img-001.png

Material-UI は、マテリアルデザインを提供する React コンポーネントライブラリです。

Material-UI のインストール

Material-UI のコアパッケージ (@material-ui/core) は、npm コマンドで簡単にインストールできます。 マテリアルデザイン系のアイコン を使いたい場合は、@material-ui/icons パッケージもインストールしておきます。

$ npm install @material-ui/core
$ npm install @material-ui/icons

Next.js の create-next-app コマンドでプロジェクトを作成済みであれば、これだけで Material-UI コンポーネントを使う準備は完了です。 Material-UI のデフォルトテーマは Roboto フォントを使用する ので、次のようなコードを head 要素内に記述する必要がありますが、これは後述の _document.tsx で設定します。

<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />

Material-UI は font weight に 300/400/500/700 のいずれかを使用するので、上記のように読み込むデータを制限することで、ロード時間を削減できます。

Material-UI のコンポーネントを使ってみる

Material-UI のインストールができたら、あとは、各コンポーネントの実装ファイルから import するだけで使用できます。 次の例では、Button コンポーネントと、ButtonGroup コンポーネントを使っています。

/p/s6djqw3/img-002.png
pages/index.tsx
import { Button, ButtonGroup } from '@material-ui/core'

export default function Home() {
  return (
    <>
      <div style={{ margin: '0.5em' }}>
        <Button variant="contained">Default</Button>{' '}
        <Button variant="contained" color="primary">Primary</Button>{' '}
        <Button variant="contained" color="secondary">Secondary</Button>{' '}
        <Button variant="contained" disabled>Disabled</Button>{' '}
        <Button variant="contained" color="primary" href="https://google.com/">LINK</Button>
      </div>
      <div style={{ margin: '0.5em' }}>
        <ButtonGroup variant="contained" color="primary" aria-label="contained primary button group">
          <Button>One</Button>
          <Button>Two</Button>
          <Button>Three</Button>
        </ButtonGroup>
      </div>
    </>
  )
}

Next.js 用の App / Document コンポーネント設定

Material-UI を Next.js アプリから使用する場合は、サーバーサイドレンダリングとの兼ね合いで、pages/_app.tsxpages/_document.tsx を作成して、スタイル定義の処理順序を制御しておく必要があります。 これを入れておかないと、makeStyle などを使ったスタイル設定 がうまく反映されず、次のようなエラーになったりします。

Warning: Prop `className` did not match.
Server: "MuiTypography-root makeStyles-customText-5 ..."
Client: "MuiTypography-root makeStyles-customText-1 ..."

これは、サーバーサイド側で生成された CSS クラス名が、クライアント側で参照しようとしているクラス名と食い違ってしまった場合に発生します(クラス名を自動生成する仕組みのため、タイミングによって発生します)。 具体的な対応方法に関しては、Material-UI のドキュメントには下記のコードを参照、と書かれていますが、こちらは残念ながら TypeScript に対応していないので、ここでは TypeScript 化したコードを載せておきます。

pages/_app.tsx
import React from 'react'
import { AppProps } from 'next/app'
import Head from 'next/head'
import { CssBaseline } from '@material-ui/core'
import { ThemeProvider } from '@material-ui/core/styles'
import theme from './theme'

export default function MyApp({ Component, pageProps }: AppProps): JSX.Element {
  React.useEffect(() => {
    // Remove the server-side injected CSS.
    const jssStyles = document.querySelector('#jss-server-side')
    jssStyles?.parentElement?.removeChild(jssStyles)
  }, [])

  return (
    <>
      <Head>
        <title>MyApp</title>
        <meta
          name="viewport"
          content="minimum-scale=1, initial-scale=1, width=device-width"
        />
      </Head>
      <ThemeProvider theme={theme}>
        <CssBaseline />
        <Component {...pageProps} />
      </ThemeProvider>
    </>
  )
}

pages/_document.tsx では、Roboto フォントの読み込みも行っています。

pages/_document.tsx
import React from 'react'
import Document, {
  DocumentContext,
  DocumentInitialProps,
  Html,
  Head,
  Main,
  NextScript,
} from 'next/document'
import { ServerStyleSheets } from '@material-ui/core/styles'
import theme from './theme'

export default class MyDocument extends Document {
  render(): JSX.Element {
    return (
      <Html lang="ja">
        <Head>
          {/* PWA primary color */}
          <meta name="theme-color" content={theme.palette.primary.main} />
          <link
            rel="stylesheet"
            href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
          />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    )
  }

  // `getInitialProps` belongs to `_document` (instead of `_app`),
  // it's compatible with server-side generation (SSG).
  static async getInitialProps(
    ctx: DocumentContext
  ): Promise<DocumentInitialProps> {
    // Render app and page and get the context of the page with collected side effects.
    const sheets = new ServerStyleSheets()
    const originalRenderPage = ctx.renderPage

    ctx.renderPage = () =>
      originalRenderPage({
        enhanceApp: (App) => (props) => sheets.collect(<App {...props} />),
      })

    const initialProps = await Document.getInitialProps(ctx)

    return {
      ...initialProps,
      // Styles fragment is rendered after the app and page rendering finish.
      styles: [
        ...React.Children.toArray(initialProps.styles),
        sheets.getStyleElement(),
      ],
    }
  }
}

上記のファイルからテーマ設定ファイル theme.ts を参照しているので、これも作成しておきます。 サイト全体のプライマリカラーや、セカンダリカラーをここで設定できます。

pages/theme.ts
import { createMuiTheme } from '@material-ui/core/styles'
import { red } from '@material-ui/core/colors'

const theme = createMuiTheme({
  palette: {
    //primary: {
    //  main: '#556cd6',
    //},
    //secondary: {
    //  main: '#19857b',
    //},
    error: {
      main: red.A400,
    },
    background: {
      default: '#fff',
    },
  },
})

export default theme

関連記事

macOS のスポットライトのインデックス処理を停止・開始する

更新:
作成:

macOS の mdutil コマンドを使用すると、スポットライトのインデックス設定を変更することができます。

インデックス処理の現在の設定を調べる
$ sudo mdutil -a -s
/:
    Indexing enabled.
/System/Volumes/Data:
    Indexing enabled.
/Volumes/SD_card:
    Indexing and searching disabled.
インデックス処理を無効化する
$ sudo mdutil -a -i off
インデックス処理を有効化する
$ sudo mdutil -a -i on
インデックスを削除して再生成
$ sudo mdutil -a -E

関連記事

メニュー

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