令和時代の CSS 記法再考
昔は CSS と言えば壊れやすい代名詞のようなもので、堅牢な CSS を書くためによく BEM や FLOCSS といったCSS 設計記法の縛りを入れて書いたりしていましたが、最近は React と Scoped CSS のセットで書くことが多くて、素の HTML/CSS で設計記法を遵守した CSS を書く機会はだいぶ減りました。
衝突を気にしなくて良い環境で CSS を書いているので BEM (Block/Element/Modifier) で言うところの Element
あたりはあまり気にならなくなりましたが、Modifier
の扱いに関しては引き続き現役というか、むしろコンポーネントの Props などでより意識をする機会が多くなったように感じます。
ちなみに 自分は CSS in JS に関しては若干距離を置いてるというか、Scoped CSS が実現できれば十分で、ユーザーに負担を負わせないという意味でゼロバンドルである CSS Modules や vanilla-extract を主に好んで使っています。
BEM で言うところの Modifier の書き方を見直す
さて、今まで Modifier を HTML/CSS で表現するとこんな感じになると思います。
<button
type="button"
class="button button--primay button--is-pressed"
>
ボタン
<button>
.button {
color: #333;
background-color: #f0f0f0;
border: #999 1px solid;
padding: .4em;
&--primay {
background-color: #fcc;
}
&--is-pressed {
box-shadow: 2px 2px 4px #999 inset;
}
}
これを自分は、
- 極力 WAI-ARIA 属性で表現する
- WAI-ARIA で表現できないものはカスタムデータ属性で表現する
といった書き方を推奨しています。
<button
type="button"
class="button"
data-level="primary"
aria-pressed="true"
>
ボタン
<button>
.button {
color: #333;
background-color: #f0f0f0;
border: #999 1px solid;
padding: .4em;
&[data-level="primay"] {
background-color: #fcc;
}
&[aria-pressed="true"] {
box-shadow: 2px 2px 4px #999 inset;
}
}
ただそれだけなんですが、こちらのほうが従来の BEM 的な書き方よりも優れている点がいくつかあります。
メリット1: HTML/CSS の見通しが良くなる
従来の方法だと スタイリングがすべて class 属性に集約されるため、Modifier が多いと HTML がとても読みにくくなりますが、この方式は Modifier が属性ごとに分割されているため、よりリーダブルなコードになります。
CSS も属性セレクタで定義されているものが Modifier であることが明示的になります。
CSS 詳細度は BEM と比べて上がりますが、このルールで統一されている以上はそれほど問題になることはないので受容としています。
また、React の JSX では従来の記法では clsx などを使って以下のように className を合成しますが、
function Button({ level, isPressed }) {
return (
<button
type="button"
className={clsx(style.button, level, { 'is-pressed' : isPressed })]
>
ボタン
<button>
)
}
推奨記法では Props と属性が対となるので受け渡しも容易、コードも宣言的で読みやすいです。
もちろん className 合成のためのパッケージも不要です。
function Button({ type, isPressed }) {
return (
<button
type="button"
className={style.button}
data-level={level}
aria-pressed={isPressed}
>
ボタン
<button>
)
}
メリット2: アクセシビリティが向上する
WAI-ARIA には要素の状態を示す様々な属性があります。
表示/非表示を示す aria-hidden
、UI の選択状態を示す aria-selected
, aria-checked
, aria-pressed
、開閉状態を示す aria-expanded
、現在地を示す aria-current
、読み込み中を示す aria-busy
など。
これらの状態は class で表現するよりも WAI-ARIA 属性を使った方が、コンポーネントの状態を正しく伝えることができるためアクセシビリティが向上します。また、属性セレクタを使えば WAI-ARIA 属性も CSS から参照できます。
React では状態と WAI-ARIA 属性を紐づけることで HTML/CSS/JavaScript すべての参照を WAI-ARIA 属性に集約できるため、より効率的にコンポーネントを管理できます。
さらに、昨今のフロントエンドではテストを書く機会が多くなったと思いますが、ユニットテストで使われる Testing Library や E2E テストで使われる Playwright でも getByRole
のオプションでは WAI-ARIA 属性を検索条件に加える事ができます。
そのため、積極的に WAI-ARIA 属性を利用することでテスタビリティも向上します。
以上の理由から、ユーザーにとっても開発者にとってもメリットが大きいので自分はこの記述方法を推奨しています。
ダイナミックスタイリングの書き方を見直す
コンポーネントの中には静的な CSS だけでは表現できないケースがあります。
例えばダイアログやポップアップ、ツールチップのようなレイヤーを重ねて表示するものは、JavaScript で動的に計算した座標を出力する必要があります。
その場合、今までは style 属性でプロパティを直接記述して座標をコントロールしていました。
<div
class="tooltip"
aria-hidden="false"
style="top: 120px; left: 40px"
>…中略…</div>
.tooltip {
position: absolute;
}
これでも動作としては問題はないんですが、より良い書き方として、直接 style 属性にスタイルを書かずに CSS カスタムプロパティを使って以下のように書き直しています。
<div
class="tooltip"
aria-hidden="false"
style="--top: 120px; --left: 40px"
>…中略…</div>
.tooltip {
--top: 0;
--left: 0;
position: absolute;
top: var(--top);
left: var(--left);
}
わざわざ CSS カスタムプロパティを経由するという冗長なことをしているように見えますが、これによってコード管理上のメリットがあります。
メリット1: CSS と style 属性の混在による詳細度の差異を緩和する
基本は CSS ファイルに書かれたプロパティよりも style 属性に書かれたプロパティの方が詳細度が高いため style 属性の定義が常に優先されます。そのため、通常 CSS ファイルに書かれたプロパティは style 属性に勝つことはできません。
今回の記述で CSS カスタムプロパティを経由することで style 属性勝ちのルールを覆すことができます。つまり、詳細度を CSS ファイルの中の定義だけで完結させることができます。
また、この CSS カスタムプロパティはコンポーネント単位にスコープが分かれているため、同じコンポーネントをたくさん HTML に記述しても、style 属性の CSS カスタムプロパティは自身以外のコンポーネントに影響を与えません。
メリット2: ダイナミックスタイリングの影響範囲を CSS ファイルの中で宣言的に定義できる
HTML 側から style 属性で CSS プロパティを上書きする従来の方法は、CSS ファイルの定義と HTML 側の style 属性の内容の両方を見て、合成される CSS プロパティを想像したり、ブラウザの computed style から情報を得る必要がありました。
これを CSS カスタムプロパティを経由することで、CSS ファイルは外から注入される CSS プロパティを予め定義しておく形になります。
.tooltip {
/* 外部から挿入されるパラメータの宣言 */
--top: 0;
--left: 0;
/* スタイル本体 */
position: absolute;
top: var(--top);
left: var(--left);
}
結果的に CSS ファイルを見るだけで、どのプロパティがダイナミックスタイリングの対象となるのかが一目瞭然になり、より宣言的でリーダブルな CSS になります。
メリット3: 疑似要素に対してもダイナミックスタイリングを適用できる(2024/05/03 追記)
style 属性からスタイルを指定する従来の方法では対象となる要素に直接スタイルを適用することになりますが、CSS カスタムプロパティを経由すると、その要素を起点とした子要素にまでその影響を与えることができます。
その要素が ::before
, ::after
のような疑似要素を内包している場合は、それも CSS カスタムプロパティのスコープに含まれるため、従来の style 属性では適用できなかった疑似要素に対しても動的なスタイルを適用することができます。
<button type="button" class="button" style="--icon: '★'">ボタン</button>
.button {
/* 外部から挿入されるパラメータの宣言 */
--icon: "";
/* スタイル本体 */
display: flex;
align-items: center;
text-align: left;
}
/* 疑似要素に対して CSS カスタムプロパティを適用する */
.button::before {
content: var(--icon);
}
従来の style 属性によるダイナミックスタイリングでは疑似要素へのスタイル適用はできないので、完全な上位互換としてこの記法を利用することができます。
まとめ
- スタイルの定義を class に集約するよりも WAI-ARIA 属性とカスタムデータ属性を使った方が、よりリーダブルな HTML(JSX)になります
- WAI-ARIA 属性を積極的に使うことで、アクセシビリティとテスタビリティの向上を狙えます
- ダイナミックスタイリングは CSS のカスタムプロパティを使うことで CSS プロパティの定義を CSS ファイルに集約、詳細度の平準化とよりリーダブルな CSS を実現できます
CSS 記法というにはちょっと大げさかもしれませんが、一つの考え方としてアウトプットしておきます。CSS を書く上で何らかの参考になれば幸いです。
Discussion
最近の流れとして、CSS in JS はオワコンということで tailwind で書いていますが、className をゴニョゴニョしてユーティリティ化するメリットを見いだせないところにこの記事を拝見しました。いいですね。
WAI-ARIA 属性とカスタムデータ属性、この観点で UI を作り込むことがアクセシビリティとテスタビリティの向上につながる、というのは理にかなっていると感じます。
デザイナーがこの辺りの観点を以てリザルトしてくれることを期待しますね。
平面デザインではなくWebデザインができる方が少ないような気がしています。
*携わるプロジェクトのせいにしてみるw
こちらの記事はバニラCSSかくあるべし的な観点でアウトプットされたものと推測します。
知識を蓄積しそれを考察するのは大変有意義だと感じます。
良い時間が過ごせました。ありがとうございました。
コメントありがとうございます。
CSS の面倒くさいところ(詳細度やスタイルの衝突、依存関係)を一切気にせず、バンバン追加削除しても良いユーティリティ系 CSS にもメリットはあると思いますが、自分も古い人間なので、HTML は文書構造を定義し、CSS は スタイルを定義するといった責務を明確に分ける書き方の方が性に合ってます。
どっちが優れているかというよりは、もう好みの問題な気もしますが…
CSS in JS を使っても最終的には Vanilla CSS に変換されるし、React や Vue.js を使っても最終的には HTML が出力されるので、最終的に吐き出される HTML/CSS を意識して書いていきたいですね。
記事を読ませていただきました、CSSをよりセマンティックに書くというのはなかなか伝わりづらい考え方かと思いますが端的に内容がまとまっており良い記事だと感じました。
しかしながら、一部気になった点があり書かせていただきます。
本文中でこのように述べられておりましたが、アクセシビリティ関連の勉強会では不用意にARIA属性を使わないほうがよい(要素が本来持っているセマンティックを明示的に記述しない)と言われている印象がありその点について注意書きなどがあったほうが良いのではないかと思いました。
また具体例として、このような書き方をしてしまう方が現れるのではないかと思いました。
このような場合擬似クラスを使ったCSSの書き方のほうが適切なため、最初に述べられていた順位付けの上に擬似クラスで指定できるものは擬似クラスを使用するという項目が入ってくるのではないかと感じました。
私もアクセシビリティに関してはそこまで深い知識があるわけではないため、認識が間違っている場合などもあるかと思いますがご確認頂けますと幸いです🙇♂️
コメントとご指摘ありがとうございます。
要素が本来持っている属性の役割と重複している WAI-ARIA 属性は使わないことを当たり前の感覚で記事を書いていましたが、確かに文面上から読み取れないので注記と言うかたちで補足を入れておきました。