Closed26

Next.jsにCSS Modulesを導入する

catnosecatnose

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

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

というわけでやってみる

catnosecatnose

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>
  )
}
catnosecatnose

.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)のパターンでいくことにした。

catnosecatnose

stylesディレクトリ内の構造

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

styles/global.scss

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

styles/components

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

styles/layouts

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

styles/variables.scss

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

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

カラーコードはCSS変数で

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

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

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

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

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

問題はメディアクエリ。毎回@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;
  }
}
KiteKite

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;
  }
}
catnosecatnose

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

uttkuttk

相対パスで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を使っている方なら、すぐに導入できるので覚えておくと便利だと思います💪

catnosecatnose

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

catnosecatnose

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

catnosecatnose

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

yusei_wyyusei_wy

既にご存知かもですが、共有します。
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 がパスカルケースなどのクラス名に対応していないことです。

catnosecatnose

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

catnosecatnose

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}>
catnosecatnose

CSS Modulesでアニメーション

普通に@keyframesが使える。

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

.container {
  animation: foo 0.3s ease;
}
catnosecatnose

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

https://github.com/lukeed/clsx

catnosecatnose

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

catnosecatnose

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

catnosecatnose

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

Before:styled-components

After:CSS Modules

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

nap5nap5

@useを使ってNextJsにCSS Modulesを導入したデモを作ってみました。

エイリアスがうまくtyped-scss-modulesライブラリのオプション引数で制御できず、以下のようにしています。

@forward 'src/styles/mq';
// @forward '@/styles/mq';

オプション引数でこうするとうまくいっているように見えました。デモもちょこっと直してみました。
@始まりのパスをsrc始まりのパスにマッピングする場合)

--aliasPrefixes.@ src/

demo code.
https://codesandbox.io/p/sandbox/damp-tdd-400j5x?file=%2Fsrc%2Ffeatures%2Fhome%2Fcomponents%2FHome%2FHome.tsx

demo site.
https://400j5x-42649.csb.app/

簡単ですが、以上です。

このスクラップは2020/12/08にクローズされました