Help us understand the problem. What is going on with this article?

CSS modulesを使ってCSSで名前空間を実現する

More than 1 year has passed since last update.

CSSは、勉強してないのをごまかすためにイキって強がる人の風評被害を受けやすく、実際よりも低く評価されてしまうことがよくあります。
でも現代のCSSは意外に良いものですし、依然として残る課題に対しても「名前空間がないからCSSマジつかえねぇわ〜」と知ったかぶりして書かない理由を探すのではなく、やはり生産的に解決したいものです。

そこで、この記事ではCSS modulesを使って お手軽に CSSに名前空間を擬似的に導入します。
また、ParcelでCSS modulesを使う時に遭遇するバグの回避方法もあわせてお見せします。

CSSに名前空間がないことで起きる問題

CSSには名前空間がないため、たとえばこれからご紹介する例のように複数のCSSファイルを読み込んだ際に意図せぬ見た目になってしまうことがあります。

まず、以下の yagi.css だけを読み込んでみましょう。

yagi.css
.yagi {
  width: 10em;
}

.big {
  width: 50em;
}

.yagi::after {
  content: '';
  display: block;
  background-image: url('https://user-images.githubusercontent.com/1481749/56465716-251ebf00-643f-11e9-8c66-8d0de8953663.jpg');
  background-size: contain;
  background-repeat: no-repeat;
  padding-top: calc(100% * 225 / 400);
  width: 100%;
}
yagi.html
<!DOCTYPE HTML>
<html>
<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="yagi.css">
</head>
<body>
  <div class="yagi"></div>
  <div class="yagi big"></div>
</body>
</html>

yagi0.png

yagi.css に設定した .big クラスの設定が効いて $\huge{大きなヤギ}$ の画像になっています。

次に、以下の button.css だけを読み込んでみましょう。

button.css
.button {
  padding: 0.6em;
  font-size: 1em;
}

.big {
  font-size: 3em;
}
button.html
<!DOCTYPE HTML>
<html>
<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="button.css">
</head>
<body>
  <button type="button" class="button">Button</button>
  <button type="button" class="button big">Big Button</button>
</body>
</html>

button0.png

button.css に設定した .big クラスが効いてボタンが大きくなっています。

では、yagi.cssbutton.css を両方読み込むとどうなるでしょうか。

mix.html
<!DOCTYPE HTML>
<html>
<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="yagi.css">
  <link rel="stylesheet" href="button.css">
</head>
<body>
  <button type="button" class="button">Button</button>
  <button type="button" class="button big">Big Button</button>
  <div class="yagi"></div>
  <div class="yagi big"></div>
</body>
</html>

mix.png

ぶっこわれました。

ヤギ画像にもボタンにも以下の 両方の CSSが適用されてしまったためです。

yagi.css
.big {
  width: 50em;
}
button.css
.big {
  font-size: 3em;
}

このように、一般的過ぎるクラス名を使うと、意図しない要素にそのCSSが影響してしまいます。

BEM記法

技術ではなく運用でこの問題を解決しようとするのがBEM記法です。
先ほどの例では、以下のようにクラス名をつけなおします。

yagi-bem.css
.yagi {
  width: 10em;
}

.yagi--big {
  width: 50em;
}

.yagi::after {
  content: '';
  display: block;
  background-image: url('https://user-images.githubusercontent.com/1481749/56465716-251ebf00-643f-11e9-8c66-8d0de8953663.jpg');
  background-size: contain;
  background-repeat: no-repeat;
  padding-top: calc(100% * 225 / 400);
  width: 100%;
}
button-bem.css
.button {
  padding: 0.6em;
  font-size: 1em;
}

.button--big {
  font-size: 3em;
}

手作業でクラス名の頭に識別子をちまちまくっつける感じですね!
悪くはないんですが、識別子を付けないやつがチームにいると困ったことになります。

また、いくらベラのキャラデザインを現代的な若い女の子にしても、同時期に放映されているゲゲゲの鬼太郎もネコ娘のキャラデザインが一新されており、脚本も攻めまくったおもしろい内容になっているので「まぁ鬼太郎があるし、妖怪人間の方は見なくていいか」となってしまいます。

CSS modules で解決する

そこで、人間の手にたよらずに、ヤギの手も妖怪の手も借りずに自動で名前の衝突を避けてくれる技術の1つが CSS modules です。

では、お手軽にためしてみましょう。
nodeをインストールしていない方は先にインストールしておいてください。
以下はLinux OS上での操作を想定していますが、Macでもだいたい一緒だと思います。
うぃんどうずを使っている人はまずデュアルブートでUbuntuとかを入れましょう。

css-modules-sample みたいなディレクトリを作り、そのディレクトリの中で

$ npm i postcss-cli postcss-modules

を実行して必要なライブラリをローカルインストールしましょう。
あとは、CSSファイルと以下の設定ファイルをこのディレクトリに置いておきます。

(設定ファイルの詳しい仕様を知りたくなったら公式ドキュメントをご参照ください)

postcssrc.js
module.exports =
  {
    "modules": true,
    "plugins": {
      "postcss-modules": {},
    }
  }

これで準備は完了です。
このディレクトリ内に yagi.css を用意して、以下のコマンドを実行してみましょう。

$ npx postcss yagi.css -o dist/yagi.css

あらたに yagi.css.jsondist/yagi.css が生成されているはずです。
dist/yagi.css をひらいてみると、以下のような内容になっています。

dist/yagi.css
._yagi_1d9sa_1 {
  width: 10em;
}

._big_1d9sa_5 {
  width: 50em;
}

._yagi_1d9sa_1::after {
  content: '';
  display: block;
  background-image: url('https://user-images.githubusercontent.com/1481749/56465716-251ebf00-643f-11e9-8c66-8d0de8953663.jpg');
  background-size: contain;
  background-repeat: no-repeat;
  padding-top: calc(100% * 225 / 400);
  width: 100%;
}

なんかランダムっぽい識別子がくっついて名前の衝突を防げるようになっていますね?

yagi.css.json は、オリジナルのクラス名がCSS modulesによってどのように変換されたかをJSON形式で保持しています。

yagi.css.json
{"yagi":"_yagi_1d9sa_1","big":"_big_1d9sa_5"}

なんらかの工夫をして、HTML側のタグにつけるクラス名もこのJSONファイルを見て変換しないといけません。
Reactとかだと何かそういうライブラリとかがあるみたいです。
でも、ちょっとそれってめんどくさいですよね?
そこでそんなめんどくさがりの人のために、ずるいやり方 があります。

設定ファイル postcssrc.js を以下のように書き換えてしまってください。

postcssrc.js
module.exports =
  {
    "modules": true,
    "plugins": {
      "postcss-modules": {
        "generateScopedName": "[name]__[local]",
        "getJSON": () => null
      }
    }
  }

postcss-modulesの説明にあるとおり、

  • generateScopedName は「どのようにクラス名を変換するか」のルールを指定し
  • getJSON は変換の対応表の書き出し方を指定します

ここでは generateScopedNameファイル名__元のクラス名 という形式を指定し、getJSON には対応表は特に生成しないように指定しています。
実行して結果を見てみましょう。

$ npx postcss yagi.css -o dist/yagi.css
dist/yagi.css
.yagi__yagi {
  width: 10em;
}

.yagi__big {
  width: 50em;
}

.yagi__yagi::after {
  content: '';
  display: block;
  background-image: url('https://user-images.githubusercontent.com/1481749/56465716-251ebf00-643f-11e9-8c66-8d0de8953663.jpg');
  background-size: contain;
  background-repeat: no-repeat;
  padding-top: calc(100% * 225 / 400);
  width: 100%;
}

これなら事前にどのようなクラス名に変換されるか予測できるため、HTML側を書く時に

mix2.html
<!DOCTYPE HTML>
<html>
<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="yagi.css">
  <link rel="stylesheet" href="button.css">
</head>
<body>
  <button type="button" class="button__button">Button</button>
  <button type="button" class="button__button button__big">Big Button</button>
  <div class="yagi__yagi"></div>
  <div class="yagi__yagi yagi__big"></div>
</body>
</html>

としておけばいいだけです。

Elmを使っている方は、「Elmでもっと気軽にCSS modules」で紹介した方法を使うと簡単です。

Parcelで使う

ファイルを結合したり、Babelを使ったり、Minifyしてくれたり、そういう面倒なタスクをお手軽にいろいろ勝手にやってくれる技術の1つにParcelがあります。
Parcelを使う場合は先ほどの .postcssrc.js をディレクトリ内に置いた状態で

$ npm i parcel https://github.com/arowM/parcel/archive/parcel-bundler@1.12.4.tar.gz
$ parcel build mix2.html --public-url ./

とすれば、dist にコンパイル後のファイルが生成されます。

サンプルリポジトリ を作っておいたので、このリポジトリをcloneして

$ npm i && npm run build

としても同じことができます。

mix2.png

この通り、うまくいきました!

なお、Parcel には postcss-modules の generateScopedName が効かないという問題があり、これを修正したものがまだリリースされていません。
そこで、上記のコマンドやサンプルリポジトリでは、parcelをフォークして独自に修正した parcel-bundler@1.12.4 をインストールしています。

さくらちゃんにご飯をあげる
さくらちゃんをもっと見る
他の記事を見る
sakura.jpg

arowM
ヤギさんとして自由に生きてるよ さくらちゃんはアーティストだから世の理不尽には頭突きしちゃうよ フリーランスUXハッカー・プログラマー(Elm, Haskell)・技術翻訳・ヤギ語翻訳 ARoW代表 http://arow.info /気吹堂(出版)代表/人材紹介会社CXO http://github.com/arow
https://arow.info
arow-oss
ヘテロジニアスで自律分散型な優しい会社です。 従業員がヘテロジニアスな自律分散ノードとして活躍し、代表が汎用ノードとして全体の仕事の調整や、割り振り先がない仕事の処理を担当するボロ雑巾として活躍してます。 フロントエンド側のヘテロジニアスノードがほしいなー
https://arow.info
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away