Next.jsにCSS Modulesを導入する
ZennのフロントのCSSをどうするか問題。
linariaは一旦様子見ということで中断したが、やはりstyled-componentsだとパフォーマンスが気になる。
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つの選択肢が考えられる。
components
ディレクトリと同じ階層に置く
1. components/Button.tsx
のスタイルはcomponents/Button.module.scss
に書くパターン
styles
のようなディレクトリを作ってcomponents
と同じ階層で配置
2. components/Button.tsx
のスタイルはstyles/components/Button.module.scss
に書くパターン
Zennの場合にはcomponents
ディレクトリのファイル数がけっこう多いので、見通しをよくするために(2)のパターンでいくことにした。
stylesディレクトリ内の構造
styles
ディレクトリの中はさらにこんな感じで分けた。
styles/global.scss
- ここにグローバルに(アプリ全体で)読み込みたいスタイルを書く。
-
_app.tsx
でimport styles/global.scss
するだけで読み込み設定は完了。
styles/components
- コンポーネント用の
.module.scss
を入れてく
styles/layouts
- Zennではレイアウト用のコンポーネントは
layouts
ディレクトリに入れてるので
styles/variables.scss
- scssファイルで使いまわしたい変数をここに入れる
CSS Modulesで変数をどう管理するか
カラーコードはCSS変数で
カラーコードなどのCSSプロパティの変数は、グローバルに読み込むCSSで設定すればOK。
:root {
--c-primary: #3ea8ff;
}
で、各module.scss
からこれを使う。
.primary {
background: var(--c-primary);
}
メディアクエリの変数を共有する
問題はメディアクエリ。毎回@media only screen and (max-width : 480px) {}
とか書きたくないし、直接数字を管理したくない。
↓ というわけでstyles/variables.scss
に以下のようにメディアクエリのmixinを書いておく。
$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;
}
}
↓ 各ファイルから使う
@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.js
のsassOptions
をいじって対応
以下のようにstyles
ディレクトリをincludePaths
に追加する。
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 と関係なくて、また揚げ足取りのようで申し訳ありません、以下のように書くともっとスッキリすると思いますがいかがでしょう?
$breakpoints: (
'sm': 576px,
'md': 768px,
'lg': 992px,
'xl': 1200px,
);
@mixin mq($size) {
@media screen and (max-width: #{map-get($breakpoints, $size)}) {
@content;
}
}
あ、完全に仰る通りです・・・!スッキリしました。ありがとうございますー!
Next.js
が依存している sass-loader
は dart-sass
を標準で採択しているので、新規コードを書くならば廃止予定の @import
を使用するよりも @use
や @forward
を使用したほうが保守性の面から見て安心かなぁと思います。もしよろしければご検討くださいませ。
以下のエントリーに詳しいです。
相対パスで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
{
"compilerOptions": {
/* -- 省略 -- */
"baseUrl": "./src",
"paths": {
"@variable": "styles/variable.scss"
},
/* -- 省略 -- */
}
}
/* 比較の為に @import文 を使ってます。ご了承ください。*/
/* ./srcからの位置で書ける */
@import "styles/variable.scss";
/* pathsも使える */
@import "@variable";
/* -- 省略 -- */
// 勿論、ts,tsxファイルでもエイリアスが効いてる
import { Test2Component } from "components/Test2";
// ./srcからの位置で書ける
import styles from "styles/components/Test.module.scss";
// 一応出来るが、別途型定義ファイルや設定が必要になる
import variableStyles from "@variable";
/* -- 省略 -- */
参考
TypeScriptを使っている方なら、すぐに導入できるので覚えておくと便利だと思います💪
ベンダープレフィックス
特に設定はしていないが、flexやgrid周りなど、ちゃんとベンダープレフィックスがついてる。設定を変えるにはpostcss.config.json
でautoprefixerをいじれば良さそう。
これで一通りネックになりそうな部分を乗り越えた気がする。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
コマンドに失敗してしまいます。
module.exports = {
pageExtensions: ['page.tsx', 'page.ts', 'page.jsx', 'page.js'],
};
ちょっと残念なのは typed-scss-modules がパスカルケースなどのクラス名に対応していないことです。
おー、とても有益な情報ありがとうございます!
ワークアラウンドまでありがとうございます。一度試してみます!
CSS Modulesでmodifierを使う
BEMでいうmodifierを使いたい場合は以下のようにすれば良いらしい。
import clsx from "clsx"; // クラス名を管理しやすくするやつ
<button className={clsx(styles.button, styles.buttonLarge)}>Click Me</button>
.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 { background-color: black; }
.buttonWhite { background-color: white; }
そのため、上書きしたいような場合にはcomposes
というものを使う。
.foo {
font-size: 1rem;
background-color: #333;
}
.fooLarge {
composes: foo; /* fooのスタイルがそのままここに突っ込まれるイメージ */
font-size: 1.1rem;
}
そしてクラス名としてはどちらか片方だけをあてる。
<div className={foo}>
// もしくは
<div className={fooLarge}>
CSS Modulesでアニメーション
普通に@keyframes
が使える。
@keyframes foo {
0% {
left: 0;
}
100% {
left: 100%;
}
}
.container {
animation: foo 0.3s ease;
}
CSS ModulesをReactで使う場合はclsxを合わせて使うと書きやすくておすすめ。
実際に書いてみて思うのはclsx
を使って複数のクラスをあてたりするのはCSS Modulesではアンチパターンと言えそう。できる限り、1つの要素に対して1つのクラスだけをあてるようにする。
ようやく終わった。CSS Modulesは柔軟に書きづらいと感じたが、一貫した書き方が強いられるという点では良いかもしれない。
パフォーマンスの差をPage Speed Insightsでざっくりと計測する
Before:styled-components
After:CSS Modules
「Style & Layout」の時間が半分近くまで短縮されてるっぽい。体感的に正直分からない。
CSS Modulesがdeprecatedにならないことを願う
@use
を使ってNextJsにCSS Modulesを導入したデモを作ってみました。
エイリアスがうまくtyped-scss-modules
ライブラリのオプション引数で制御できず、以下のようにしています。
@forward 'src/styles/mq';
// @forward '@/styles/mq';
オプション引数でこうするとうまくいっているように見えました。デモもちょこっと直してみました。
(@
始まりのパスをsrc
始まりのパスにマッピングする場合)
--aliasPrefixes.@ src/
demo code.
demo site.
簡単ですが、以上です。