Closed25

Next.jsにCSS Modulesを導入する

2020/11/30時点では、Next.jsではなんとなくCSS Modulesを推しているような空気がある。ビルトインサポートしてるし。

これまでCSS Modulesから逃げ続けてきたけど、このツイートを見て、そうだよなーーーCSS Modulesやってみるかーーーって気になってきた。

というわけでやってみる

Next.js は CSS Modulesをビルトインサポートしてるので、大きな設定は不要。

SCSSを使う

Scssで書きたいので公式の説明に則ってsassを導入する。

$ yarn add sass

これだけでfoo.modules.scssが使えるようになる。あとは以下のような感じでスタイルをあてていけばOK。

import styles from './Button.module.scss'

export function Button() {
  return (
    <button className={styles.error}>
      Click me
    </button>
  )
}

.modules.scssファイルをどこに配置するか

ディレクトリ構造としては主に以下の2つの選択肢が考えられる。

1. componentsディレクトリと同じ階層に置く

components/Button.tsxのスタイルはcomponents/Button.module.scssに書くパターン

2. stylesのようなディレクトリを作ってcomponentsと同じ階層で配置

components/Button.tsxのスタイルはstyles/components/Button.module.scssに書くパターン

Zennの場合にはcomponentsディレクトリのファイル数がけっこう多いので、見通しをよくするために(2)のパターンでいくことにした。

stylesディレクトリ内の構造

stylesディレクトリの中はさらにこんな感じで分けた。

styles/global.scss

  • ここにグローバルに(アプリ全体で)読み込みたいスタイルを書く。
  • _app.tsximport styles/global.scssするだけで読み込み設定は完了。

styles/components

  • コンポーネント用の.module.scssを入れてく

styles/layouts

  • Zennではレイアウト用のコンポーネントはlayoutsディレクトリに入れてるので

styles/variables.scss

  • scssファイルで使いまわしたい変数をここに入れる

CSS Modulesで変数をどう管理するか

カラーコードはCSS変数で

カラーコードなどのCSSプロパティの変数は、グローバルに読み込むCSSで設定すればOK。

styles/global.scss
:root {
    --c-primary: #3ea8ff;
}

で、各module.scssからこれを使う。

Button.module.scss
.primary {
  background: var(--c-primary);
}

メディアクエリの変数を共有する

問題はメディアクエリ。毎回@media only screen and (max-width : 480px) {}とか書きたくないし、直接数字を管理したくない。

↓ というわけでstyles/variables.scssに以下のようにメディアクエリのmixinを書いておく。

styles/variables.scss
$sm: 576px;
$md: 768px;
$lg: 992px;
$xl: 1200px;

$breakpoints: (
  'sm': 'screen and (max-width: #{$sm})',
  'md': 'screen and (max-width: #{$md})',
  'lg': 'screen and (max-width: #{$lg})',
  'xl': 'screen and (max-width: #{$xl})'
);

@mixin mq($size) {
  @media #{map-get($breakpoints, $size)} {
    @content;
  }
}

↓ 各ファイルから使う

styles/components/Foo.module.scss
@import '../variables';

.container {
  margin: 0 2rem;

  // ↓768px以下で適用したいCSS
  @include mq(md) {
     margin: 0 1rem;
  }
}

相対パスでimportするのはしんどい

各CSSモジュールのファイルから相対パスでvariables.scssにアクセスするのはしんどい。

@import '../variables';

例えばcomponents/modal/Fullscreen.module.scssからは

@import '../../variables';

としなければならなくて、ディレクトリを整理したときとかにミスりそう。

next.config.jssassOptionsをいじって対応

以下のようにstylesディレクトリをincludePathsに追加する。

next.config.js
const path = require('path');

module.exports = {
  sassOptions: {
    includePaths: [path.join(__dirname, 'styles')],
  },
  ...
}

これでcomponents/modal/Fullscreen.module.scssから相対パスを使わずにstyles/variables.scssをimportできるようになる。

@import 'variables';

.container {
  margin: 0 2rem;
  @include mq(md) {
     margin: 0 1rem;
  }
}

CSS Modules と関係なくて、また揚げ足取りのようで申し訳ありません、以下のように書くともっとスッキリすると思いますがいかがでしょう?

styles/variables.scss
$breakpoints: (
  'sm': 576px,
  'md': 768px,
  'lg': 992px,
  'xl': 1200px,
);

@mixin mq($size) {
  @media screen and (max-width: #{map-get($breakpoints, $size)}) {
    @content;
  }
}

あ、完全に仰る通りです・・・!スッキリしました。ありがとうございますー!

相対パスでimportするのはしんどい

既にご存じかもしれませんが、tsconfig.jsonのbaseUrlを設定していれば、scssファイルでもnext.config.jsの設定無しでエイリアスを効かすことが出来ます。

※ TypeScript限定なので、JavaScriptだと従来通りnext.config.jsで設定する必要があります。

ファイル構造
├─ src
│ ├─ components
│ │ ├─ Test2.tsx
│ │ └─ Test.tsx
│ └─ styles
│   ├─ variable.scss
│   └─ components
│     └─ Test.module.scss
│
└─ tsconfig.json
tsconfig.json
{
  "compilerOptions": {
    /* -- 省略 -- */

    "baseUrl": "./src",
    "paths": {
      "@variable": "styles/variable.scss"
    },

    /* -- 省略 -- */
  }
}
Test.moduels.scss
/* 比較の為に @import文 を使ってます。ご了承ください。*/

/* ./srcからの位置で書ける */
@import "styles/variable.scss"; 

/* pathsも使える */
@import "@variable"; 

/* -- 省略 -- */
Test.tsx
// 勿論、ts,tsxファイルでもエイリアスが効いてる
import { Test2Component } from "components/Test2";

// ./srcからの位置で書ける
import styles from "styles/components/Test.module.scss"; 

// 一応出来るが、別途型定義ファイルや設定が必要になる
import variableStyles from "@variable"; 

/* -- 省略 -- */

参考

https://nextjs.org/docs/advanced-features/module-path-aliases

TypeScriptを使っている方なら、すぐに導入できるので覚えておくと便利だと思います💪

これで一通りネックになりそうな部分を乗り越えた気がする。styled-componentsからの置き換えが完了したらメリット・デメリットを書く。

Nextjs + TypeScriptでCSS Modules使うとき、クラス名の型を推論させる方法がわからぬ…
本当にこのクラス名存在する?って不安になる

試してみたけどうまくいかず…。create-react-appで作ったアプリならいい感じに動いたんだけど

既にご存知かもですが、共有します。
Next.js でも typed-scss-modules を使うことで、クラス名を補完して実在するクラスを指定することができるようになりました。

ポイントは Next.js で page として扱われるファイルの拡張子を変更することです。
これに合わせて pages/ 以下のファイル名を pages/index.tsx から pages/index.page.tsx に変更する必要があります。

typed-scss-modules は .scss と同じ階層に型定義ファイルを生成しますが、これが原因で next build コマンドに失敗してしまいます。

next.config.js
module.exports = {
  pageExtensions: ['page.tsx', 'page.ts', 'page.jsx', 'page.js'],
};

ちょっと残念なのは typed-scss-modules がパスカルケースなどのクラス名に対応していないことです。

おー、とても有益な情報ありがとうございます!
ワークアラウンドまでありがとうございます。一度試してみます!

CSS Modulesでmodifierを使う

BEMでいうmodifierを使いたい場合は以下のようにすれば良いらしい。

Button.tsx
import clsx from "clsx"; // クラス名を管理しやすくするやつ

<button className={clsx(styles.button, styles.buttonLarge)}>Click Me</button>
Button.module.scss
.button { background-color: black; }
.buttonLarge { font-size: 1.1rem; }

clsxのようなクラス名の管理を楽にしてくれるライブラリを使えば、動的に変えるのも簡単。

<button className={
  clsx(
     styles.button,
     { [styles.buttonLarge]: shouldLarge }
   )
 }
>Click Me</button>

注意したいのは.button.buttonLargeのどちらが優先度が高いかについての保証はないこと。

例えば、以下のようにして、両方のクラスを要素にあてたときに、背景色が「black」になるか「white」になるかが保証されない。

Button.module.scss
.button { background-color: black; }
.buttonWhite { background-color: white; }

そのため、上書きしたいような場合にはcomposesというものを使う。

Button.module.scss
.foo { 
   font-size: 1rem;
   background-color: #333;
}
.fooLarge {
   composes: foo; /* fooのスタイルがそのままここに突っ込まれるイメージ */
   font-size: 1.1rem;
}

そしてクラス名としてはどちらか片方だけをあてる。

<div className={foo}>
// もしくは
<div className={fooLarge}>

CSS Modulesでアニメーション

普通に@keyframesが使える。

Snackbar.module.scss
@keyframes foo {
  0% {
    left: 0;
  }
  100% {
    left: 100%;
  }
}

.container {
  animation: foo 0.3s ease;
}

CSS ModulesをReactで使う場合はclsxを合わせて使うと書きやすくておすすめ。

https://github.com/lukeed/clsx

実際に書いてみて思うのはclsxを使って複数のクラスをあてたりするのはCSS Modulesではアンチパターンと言えそう。できる限り、1つの要素に対して1つのクラスだけをあてるようにする。

ようやく終わった。CSS Modulesは柔軟に書きづらいと感じたが、一貫した書き方が強いられるという点では良いかもしれない。

パフォーマンスの差をPage Speed Insightsでざっくりと計測する

Before:styled-components

After:CSS Modules

「Style & Layout」の時間が半分近くまで短縮されてるっぽい。体感的に正直分からない。

CSS Modulesがdeprecatedにならないことを願う

このスクラップは2020/12/07にクローズされました
ログインするとコメントできます