この記事はUniposアドベントカレンダー2022の9日目の記事です!
フロントエンド開発をメインに働いています。
今回社内でも共有したCSSの歴史のざっくり話とCSS Modulesについて書いていこうと思います。
CSSの大まかな歴史
1. CSSっていつ生まれたの?
1994年、WWW(World Wide Web)生誕の地である
欧州原子核研究機構
(CERN:Conseil Européen pour la Recherche Nucléaire)
に勤務するホーコン・ウィウム・リーさんにより提唱されました。
ちなみにOperaのCTOをしていた人でもあるそうです。
そこからW3CやWHATWG(Web Hypertext Application Technology Working Group)のような団体が仕様策定をするようになりました。
2. 現在のバージョンは?
実はバージョンという概念はなくレベルで区分けされます。
1996年: Level1(CSS1)
フォントの装飾変更やホームページの背景、テキストやリスト、ボックスのプロパティが指定可能。
1998年: Level2(CSS2)
CSS1の上位互換がこの「Level2」です。表示する媒体によって自動的にスタイルシートを変更にできるようにするなどの改良が加えられた。
仕様書の定義が不明瞭であったことから、実用的ではないという結果に。。
2011年: Level 2.1(CSS2.1)
Level2の改訂版。ユーザー側で相互運用を認められなかった機能は削除され、汎用性の高い言語へと進化。
具体的には「音声ブラウザの対応」「自動スタイルシート変更」「印刷用端末への対応」など。
Level3(CSS3)
「モジュール(まとまり)」といった概念が導入され、ユーザーはモジュールを選択可。2018年時点で15のモジュールが公開中。
2021年時点ではほとんどのWEBサイトがこのLevel3のCSSを使用している。
実は、Media Queriesのような機能も2012年6月に勧告されていて、現在のWebのスタイルでレスポンシブなどができるようになったのもこの頃から。
Level4(CSS4)
2021年時点において、Level3のモジュールでも勧告に至ってないものも数多く、あくまでLevel4は草案段階。いくつかの草案はすでに公開中。
一部のブラウザでは先に実装されているものも多い。
3. CSSの使いづらさからの発明
CSSはDOMのネストに合わせて、記載がしづらかったり、変数や関数がなかったために、そこに不満を持つ人々から、コンパイルしてCSSへ変換しようというものが登場した。
Sass
すでにデファクトスタンダードな立ち位置で上記機能があり、登場時(2006年)かなり便利なものでした。
コンパイルが必要であり、コンパイラはつい最近まで数種類あって、対応できることもわずかだが違っていた。現在はdart-sassが公式推奨でそれ以外はメンテナンスや仕様の問題から非推奨になっている。
ちなみに記法はSassとSCSSがある。多くはSCSSを使っているかなという感触。
$font: Helvetica, sans-serif;
$primary-color: #ccc;
$white-color: #fff;
body {
font-family: $font;
color: $primary-color;
.container {
background-color: $white-color;
}
}
コンパイル後
body {
font-family: Helvetica, sans-serif;
color: #333;
}
body .container {
background-color: #fff;
}
LESS
Sassに触発された言語。現在見かけることない。
#header {
background-color: black;
.navigation {
font-size: 12px;
}
.logo {
width: 300px;
}
}
コンパイル後
#header {
background-color: black;
}
#header .navigation {
font-size: 12px;
}
#header .logo {
width: 300px;
}
Stylus
Node.js向けに開発された。こっちも現在は更新されてないですが、LESSよりは稀に目にする感覚。
body
color #f00
p
color #000
コンパイル後
body {
color: #f00;
}
body p {
color: #000;
}
4. 設計手法
巨大なサービスでCSSを書いていると「クラス名がかぶる!」ということで、そうした辛みを解消するために、いろいろな設計手法が出ました。
OOCSS
<!-- ベーススタイル(btn) 色(btn-primary, btn-successなど) -->
<button type="button" class="btn btn-primary">Primary</button>
<button type="button" class="btn btn-success">Success</button>
<!-- サイズ -->
<button type="button" class="btn btn-primary btn-lg">Large button</button>
<button type="button" class="btn btn-primary btn-sm">Small button</button>
<!-- 余白 -->
<div class="mt-1 px-2 py-3"></div>
構造・見た目を分離して定義することが思想的には強くて、クラス名・ルールを覚えて仕舞えば、レゴみたいに組み合わせることができることが強みです。
BootStrapとかも、この思想が引き継がれているかなと。(最近だとtailwind CSSも)
SMACSS
コンポーネントの抽出作業をしやすくするために、指針となるカテゴリを提案
そのカテゴリごとでルールを決めて、カテゴリ外からは影響がないことを担保していた。
構成(カテゴリ)
- Base
- デフォルトのスタイルでサイト全体に影響
- 要素セレクタに対して定義する
- normalize.cssなどもBaseに該当
- Layout
- ページをエリアごとに分割
-
header
footer
main
sidebar
など - 命名規則
-
l-
で始める(l-header
)
-
- Module
- 再利用可能なパーツ
- State
-
module
layout
の状態 -
active
hidden
など - 命名規則
-
is-
で始める(is-active
)
-
-
- Theme(任意)
- デザインテーマ
BEM
Block, Element, Modifierで構成
構成
-
Block
構成のルートとなる要素 -
Element
Block内の子要素 -
Modifier
変化した状態
.navigation { }
.navigation--left { }
.navigation__content { }
.navigation__item { }
.navigation__item--active { }
結構プロジェクトによって、ルール崩壊を起こしているイメージがBEMでした。厳格さを要求するため、ルール浸透が難しい設計でした。
FLOCSS
構成
-
Foundation
SMACSSのBaseと同じ -
Layout
SMACSSのLayoutとほぼ同じ
SMACSSと違いgridはLayoutに含めない -
Object
- Component
- 再利用可能なパーツ
-
grid
button
form
media
など - 命名規則
-
c-
で始める(c-button
)
-
- Project
- プロジェクト固有のパーツ
-
articles
ranking
comments
profile
login-form
など - 命名規則
-
p-
で始める(p-profile
)
-
- Utility
- わずかなスタイル調整のための便利クラス
-
display
margin
など - 命名規則
-
u-
で始める(u-mbs
)
-
- Component
昔を振り返ると結果として、FLOCSSにみんな行きついてたかなと。
CSS Modules・Atomic Design以前の最後にフォーカスされた設計方法。
5. CSS ModulesとAtomic Design
CSS Modules
SPAが誕生して、ReactやVue、Elmが出始めた2015年あたりにcss-loaderの実験的な機能として、CSS Modulesが作られました。
今までCSS側で命名がかぶることを考えて、ものすごい名前をつけることに躍起になってましたが、それはコンパイル時にかぶらないようにすればいいのでは?という発想で、とても画期的なものでした。
それによって、現在上記にあるような設計手法は比較的廃れていく傾向になってます。
.container {
width: 100%;
}
import style from '../xxx/xxx.scss';
const Container = () => {
return (
<div class={style['container']}>
...
</div>
)
}
上記サンプルですが、このようにすることで、containerがcss-loaderでハッシュ値などに変換され、React側もそれに合わせてハッシュ値を使用できるようになりました。(仕組み端折ります)
これによって、命名規則の縛りを意識せずともスタイルをかけるようになりました。
Atomic Design
CSS ModulesやSPAが出たことによって、パーツのカプセル化ができるようになりました。
それによって、どの粒度でカプセル化するのかという考え方としてデザインシステム含めてAtomic Designという考え方が出ました。
これによって、再利用可能性を高めていくことができ、組み立てるだけで生産ができるという状態になってきました。
UI管理ライブラリのStorybookなどもその動きをアシストすることになったと思います。
6. 現在のCSS
最近だとCSS in JSだったり、CSS自体も新しい機能を備えてCSSだけでカプセル化できるような流れになってきています。
これからスタイルは簡単に書きやすくなったり、できることも増えていくと思います!
CSS Modulesのお話
css-loaderを使って、CSS Modulesを使うことができます。SCSSを使うならsass-loaderとかを使います。
Reactとかは元々使いやすいように用意されているので、あまり設定などは意識しないかもしれません。
ただcss-loaderのことを知っておくと、どのように実現されているかのイメージがつくキッカケにはなります。
設定
module.exports = {
module: {
rules: [
{
test: /\.s[ac]ss$/i,
loader: "css-loader",
options: {
modules: {
auto: boolean | regExp | ((resourcePath: string) => boolean);
mode:
| "local"
| "global"
| "pure"
| "icss"
| ((resourcePath) => "local" | "global" | "pure" | "icss");
localIdentName: string;
localIdentContext: string;
localIdentHashSalt: string;
localIdentHashFunction: string;
localIdentHashDigest: string;
localIdentRegExp: string | regExp;
getLocalIdent: (
context: LoaderContext,
localIdentName: string,
localName: string
) => string;
namedExport: boolean;
exportGlobals: boolean;
exportLocalsConvention:
| "asIs"
| "camelCase"
| "camelCaseOnly"
| "dashes"
| "dashesOnly"
| ((name: string) => string);
exportOnlyLocals: boolean;
},
},
},
],
},
};
全部を説明すると多くなるので、興味深い内容をいくつか選んで書いていきます。
コードなどは公式のものを参考に記載しています。
scope
local
global
pure
icss
のスコープがあります。
icssはやや性質が違うので、公式に説明を任せるとして、残りの3つを扱えたらと思います。
(参考)
- pure
Reset CSSといった変化されるべきでないスコープ - global
全体のCSSファイルに影響するスコープ - local
ファイルに閉じた影響のスコープ
というような意味合いにざっくりとなってきます。
mode
上記のscopeを選ぶことが可能です。関数でファイル名のパスを用いて、細かな設定ができるようになっています。
なので、一部部分からCSS Modulesの導入を始めるといった時にglobal/localを選んでいけるようになります。
以前、導入した経験があったのですが、その際もこれを駆使して影響範囲を限定していき、リファクタリングを進めていくという活動が可能になりました。
local
scopeがlocalなものに対しての様々な設定が可能です。
localIdentName: string;
localIdentContext: string;
localIdentHashSalt: string;
localIdentHashFunction: string;
localIdentHashDigest: string;
localIdentRegExp: string | regExp;
getLocalIdent: (
context: LoaderContext,
localIdentName: string,
localName: string
) => string;
localIdentName
色々な組み合わせができるのですが、クラス名やフォルダ・パス・ファイル名・ハッシュなどが可能です。
ReactでCSS Modulesやっている場合にハッシュ値になっていたりするのはここのおかげなんですね。
これによって、CSS名設計のような複雑な作業から解放されたわけです。本当にありがたいですね。
ハッシュ値の設定については、localIdentHashSalt, localIdentHashFunction, localIdentHashDigest, localIdentHashDigestLength, localIdentContext, resourcePathをいじることで調整可能です。
実際にドキュメントを見ると色々なことができるんだ!と驚きになるので、ここでは省略しますが、ぜひ読んでいただけたらと思います。
特に調べてみるまでは意識しませんでしたが、localIdentHashDigestLengthは値が何桁かというのを選べるのですが、スケールしていくにつれクラス名衝突の可能性を感じてしまうことがあると思いますが、そこで簡単に調整できるというのは、面白かったりします。
getLocalIdent
ここでクラス名をどのように生成するかを関数ベースで決めれます。built-inで使用できるフレームワーク・ライブラリを使用していると意識しなくて良いですが、CSS Modulesによって変化したクラス名をHTML/JSと結びつける場合に、約束を作る時に必要なものになってきます。
ここでの仕組みとJavascript側を合わせることで、開発時にはクラス名をCSSと同じものを使用していき、コンパイル後にはここでの約束通りに吐き出される仕組みになります。
exportLocalsConvention
"asIs" | "camelCase" | "camelCaseOnly"| "dashes"| "dashesOnly" | ((name: string) => string)
から選択ができます。
例えば、"camelCase"を選んだ場合
.btn-primary {
color: $primary-color;
}
.btnPrimary {
color: xxx;
}
となります。何が嬉しいかというと、camelとdashesで混ざっているCSSファイルをReactで使いたいとなった時に、これを設定しておけば
<div className={btnClass["btnPrimary"]} >
xxx
</div>
のようにjsx側の記載でどちらの形式か迷わずに進めれます。
プロダクトが古くなった時のリプレイスなどの時には、非常にありがたい機能になりそうです。
(後からリファクタリングはもちろん必要ですが。。)
関数での指定も可能なので、さまざまな使い方ができそうです。
まとめ
フロントエンドでスタイルを書くという作業は、ここ数年で本当に進化しました。
今回の記事で、どういった流れでCSSの変化があったのか、今ある技術の裏にはどういったものがあるのかを振り返ることで、今後のより良い開発につながるのかなと思います。技術選定の際も、まだ手法が多いので歴史を見て決断ができるとより良いですね。
まだまだ全ての開発環境がモダンな状態でなかったり、webpackをガンガン使えないこともあると思うので、そうした方にお役に立てたら嬉しいです。
それでは、明日のアドベントカレンダーもお楽しみに!