- 下書き途中なのでそのうちまだ増える予定である。
- あくまで自分用であり、他人にこうレビューしろという意図はない。
- ある程度書ききったら非公開にする可能性もある。
-
指摘をする際は、必ず「理由」と「解決策」をセットにする(「この書き方はダメです」だけではレビューにならない)。
-
[MUST]は、仕様・コーディング規約に反するもの/バグが起きているもの/将来的に深刻な問題になりそうなもの/ユーティリティ・アクセシビリティを阻害しているものを優先して扱う。次点として「改善したほうが良いもの」を挙げる。 -
教義的になりすぎない。ユーザーやクライアントに対してメリットになりにくい/儲けにならないような趣味嗜好が入りうる指摘は
IMOとして扱うか別の機会で発信する。- 私は flex と grid の使い分けについて普段発信しているが、レビューでは挙動に問題がなければ基本的にコメントしない。
- 私は
displayの指定には複数キーワード構文を使用しているが、使用したからと言って特別なことができるわけでもないので他の人には強制しない。実務では他者にとって馴染みが深い単一構文を優先して使うこともある。- 私が複数キーワード構文を使用する理由は通常フローを
display: block flowのように明示できること、他者にレイアウトの仕組みについて教える際に複数キーワード構文を使用したほうが伝えやすいから、みたいな理由である。
- 私が複数キーワード構文を使用する理由は通常フローを
margin-inlineなどの論理プロパティを「左右ショートハンドのように扱う」ことに否定的な意見を見かけるが、私は許容している。- 前提として、通常フロー/flex/grid/コンテナクエリなど主要なレイアウト手法は、論理的な指定を前提として成立している。flex は縦書き(
writing-mode: vertical-lr)になれば主軸が変わり、grid-templateは RTL 環境ではセル順が逆転する。一方でメディアクエリ、transform、background-positionなどは物理指定しか存在しない。このような物理前提・論理前提が入り乱れた環境下でmarginやpaddingだけを取り上げて物理・論理を議論しても意味がない。 - これまで
justify-content: flex-endを「右揃え」と呼んでも問題視されにくかったのに、marginやpaddingの論理プロパティに対して突然強く批判するのは不自然に感じる。justify-contentにはrightのような物理指定も存在するが、使用されているのを見たことがない。物理・論理に拘るのであればそのあたりにも言及するべきではないだろうか。 margin-inline: autoのような使い方であれば RTL 環境でも同じ結果になりやすく、問題化しにくい。縦書きはデザイン都合でしか発生しないため、考慮対象ではない。むしろ、ブロック軸のmarginを不要に上書きしてしまう潜在的不具合を抑えられるなら多少の認識差があっても採用する価値がある。- なお、私自身は発信上のコードでは
topをinset-block-startとするなど、論理プロパティをなるべく優先して使っている。ただしこれは多言語対応のためというより、主要なレイアウト手法と整合させたいという私的な理由が大きく、他者に強制する意図はない。実務では他者にとって馴染みが深い物理プロパティを優先して使うこともある。 - CSS の有識者が多く関わっている Tailwind でさえ、 v4 から
mxがmargin-inline、pyがpadding-blockに置き換わっている。[https://tailwindcss.com/docs/margin]
- 前提として、通常フロー/flex/grid/コンテナクエリなど主要なレイアウト手法は、論理的な指定を前提として成立している。flex は縦書き(
-
意図が読み取れない記述には
[Q]を付けて、質問としてコメントする。 -
レビュイーが書いた CSS は、レビューを通した時点でレビュアーも責任を負うことを自覚する。
- プロジェクトでコーディングガイドラインが定められている場合は、それを厳守できているかを最優先で確認する。
- タイポグラフィ、カラーパレット、ボーダー、エレベーション、アニメーションイージングなどのデザイントークンで定められている定数は
tokens(Sass 系の CSS 設計法ではsettingにあたる)ディレクトリにジャンル毎にファイルを作成してカスタムプロパティに保存する。なるべくそれを指定するように勧める。tokensディレクトリは CSS から参照できるデザインガイドとして活用する.。- 一方でスペーシング系のトークンについてはあまり指摘しない。デザイントークン上では静的カンプという都合上、主に
pxかremで定義されることになる。しかし、レスポンシブ環境下での各余白は%やcqiが絡むことが多く、pxorremも<meta name="text-scale" />の存在を考慮するならば使い分けをする必要があり、二者択一で片付けられない。- Tailwind の作者も単位付きのスペーシング設計を行ったことを後悔している。
- もしもスペーシングのトークンを用意するのであればピクセル相当の数値で定義し、
calc()で各単位に変換するのが良い。
line-height: 1.8のような指定があった場合は、トークンとして定義されているものであればそれを使用するように指摘する。一方でデザイントークンとして定義されていない特異的なものであればトークンに追加せず直書きを推奨する。
- 本来この種の整形はフォーマッター/リンターの責務であり、人間が手作業で揃える仕事ではない。ただし、何らかの事情で自動化が機能していない場合は指摘する。
- インデントやプロパティ順は「統一されていること」自体が重要であり、方式そのものはプロジェクトの合意が取れていれば基本的に何でもよい。
- 私は自作の stylelint order ルールを npm に公開している:https://github.com/tak-dcxi/taks-stylelint-order
- 例として「Safari は最新メジャーバージョンの 2 つ前の最新版までを動作対象とする」という前提があるなら、現時点で Safari 17.6 で未サポートの CSS を使っていないかを確認する。
- ただし、プログレッシブ・エンハンスメントの範囲で利用でき、品質チェックでも問題になっていないのであれば指摘しない。
- プログレッシブ・エンハンスメントとして利用可能なもの
- 未サポート環境では従来どおりに表示される or フォールバックが容易であり、サポート環境ではより良い体験を提供できるもの 。
- 例:View Transition API、
text-wrap: pretty、word-break: auto-phrase、アニメーション目的の::details-content、field-sizingなど - ハーフレディングを除去する
text-box-trimも非サポートの環境でレイアウトが崩れる/表示が著しく変化するとかでなければプログレッシブ・エンハンスメントの範囲内だと考えている。
- プログレッシブ・エンハンスメントとして利用しづらいもの
- 未サポート環境で指定自体が無視されやすいアットルール、または表示崩れが大きくなりやすいレイアウト関連の機能 。
- 例:スタイルクエリ、Anchor Positioning、
position: absolute / fixedに対するplace-self、sibling-index()/sibling-count()関数など
- プログレッシブ・エンハンスメントとして利用可能なもの
- さらに、Chrome が先行実装しがちな「既存プロパティの新仕様」にも注意する。Chrome 最新版でのみ確認していると、意図せず差分にハマる可能性がある。
- ブロックレイアウトでも
justify-items/justify-selfは使用できるが、現時点で Chrome のみ。レスポンシブでgrid→blockにロールバックする場合、justify-items/justify-selfの定義が残ったままになっていないか確認する。 calc(40px / 1280px * 100vw)のような「単位付きの除算」は typed arithmetic と呼ばれ、近年各ブラウザでサポートが進みつつある。- ただし現時点では Firefox が未サポートで、Safari も 18.2 以降でのサポートに留まるため扱いづらい。利用は控える。
- 初学者が意図せず使ってしまうケースも積み上げポストなどで見かけるため、レビューでは特に注意する。
- Chrome 145 から条件下で
vwがスクロールバーを含まずに計算される挙動に変わった。:https://x.com/tak_dcxi/status/2012336570293248061- 一方で Safari / Firefox では従来どおり横スクロールが発生し得るため、
width: 100vwの使用は避ける。 - そもそも
width: 100vwは多くの場合「不要」または「別手段で置き換え可能」であり、全コアブラウザでスクロールバーを含まずに計算される挙動に置き換わったとしてもコードスメルを減らす目的で指摘対象になり得る。もっと言うのであればvw自体 Chrome でズームされないなどリスキーな単位なので控えて使用した方が良い。
- 一方で Safari / Firefox では従来どおり横スクロールが発生し得るため、
- ブロックレイアウトでも
- class やカスタムプロパティの命名に一貫性があるかを確認する。
- ケバブケース/スネークケース/キャメルケースが混在していないか。
- BEM とそれ以外の命名規則が混在していないか。
.titleのような汎用的なエレメント名は Scoped CSS によってスコープされているか確認する。- コンポーネントの値を操作する API 的カスタムプロパティは、衝突防止と異なる API 的カスタムプロパティが混在した際に「どのコンポーネントのものか」が判別できるよう、
--foregroundのような汎用名ではなく--component-name--foregroundのように対象をプレフィックスで表現する方針を勧める。 - すべてがグローバル定義になる
@keyframesも、衝突防止のためコンポーネント固有であれば同様にプレフィックスを付与することを勧める。- 教義的になる部分なのでレビューで指摘しないが、最近は
@keyframesなど「ユーザーが命名する識別子」には--を付けるように提案している。- 近年の CSS では、ユーザー定義の名前を明確に区別するために
--を強制する流れがある(例:anchor-name、animation-timelineなど)。 - 「そのプロパティでは
--が必須か?」を個別に覚える必要が減る。 - 将来 CSS 標準側のキーワードが増えても、衝突リスクを下げられる。
- 自分が定義した名前だと即座に判別できる。
- 近年の CSS では、ユーザー定義の名前を明確に区別するために
- ただし、これは値として扱われる名前に関する話であり、
@layerのように値に関係しないものであれば付与する必要はない。
- 教義的になる部分なのでレビューで指摘しないが、最近は
- 余計な指定があるほど、別セレクタ(ベースCSS/レイアウトプリミティブ/コンポーネント内の別ルール)とぶつかった際に「どれが勝つか」「どの値が残るか」が状況依存になり、期待通りに動かないケースが増える。
- 不要な指定をすると、それを打ち消すための上書きに詳細度バトルが発生して修正コストが上がる可能性がある。
- 「なんとなく指定してみる」でCSSを書くと設計判断が共有されず、再現性のない実装になりがち。宣言ひとつひとつに「なぜそれが必要か」を説明できる状態にすること。
- 特にありがちなのが「とりあえず
width: 100%をぶっこんでみる」というもの。 - 原則として、
<div>や<p>のように UA スタイルシートでdisplay: blockが適用される要素は、初期値(auto)のままで親要素の幅に自然に追従するため、多くのケースでwidth: 100%は不要である。 position: absolute/fixedが適用された要素は置換要素などを除きinset: 0を指定した時点で利用可能幅いっぱいに広がるため、width: 100%を重ねて書く必要は<img>などを除き基本的にない。height: 100%は無限ループ抑止の都合で「親要素の高さが明示されている」場合にしか期待通りに動作しない。min-heightのような提案値では成立しない。- 後述するが、明示的な高さ指定そのものがアンチパターンになり得るため、安易な
height固定は避ける必要がある。 - flex / grid アイテムは stretch が効いていれば
height: 100%を書かなくても高さいっぱいになり得るため、不要な指定になりやすい。
- 後述するが、明示的な高さ指定そのものがアンチパターンになり得るため、安易な
width: 100%/height: 100%は無意味なだけではなく、ブラウザ差分を踏んで意図しないバグを誘発することがある。- 過去にあった事例:Safari では
list-style-typeが有効な<summary>の子要素でwidth: 100%を指定をすると不自然な改行が起こる、Firefox ではheight: 100%が適用された subgrid 要素で不自然な詰めが起こる可能性がある、など。 box-sizingを初期値にロールバックする場合(例;paddingを含めずにmax-widthを設定したい場合など)や水平マージンが意図せず適用された場合にオーバーフローを起こすリスクもある。
- 過去にあった事例:Safari では
width: 100%は、外部表示形式をblockにしても自然には広がらない置換要素(例:<img>)や、フォームパーツ(例:<button>)など「必要性が明確な対象」に限定して使う。inline-blockinline-flexのような外部表示形式がinlineの要素にwidth: 100%を指定する場合は、外部表示形式をblockに変更することも検討する。この場合はvertical-alignの初期値baselineによって生じる下部の空白の発生を抑えられるメリットもある。
- 子要素を親要素の高さに揃えたいだけであれば、親要素に
display: gridを指定するだけでstretchが働き達成できることが多い。- この方法なら
min-heightでも揃えられるため、「高さ固定」というアンチパターンを回避しやすい。
- この方法なら
- Tailwind を使っている場合、何にでも
w-full/h-fullを付与していないかを確認する。特に生成 AI の出力は乱用されがち。 width: min(320px, 100%)のようなフォールバック手段としての100%なら問題ない。ただし、この場合は水平マージンの存在に気をつける必要がある。- 基本的に明示的な水平方向の余白指定は親の
paddingで行ったほうがよい。
- 基本的に明示的な水平方向の余白指定は親の
- ショートハンドは便利な一方で、ロングハンドで積み上げてきた設定が後からショートハンド指定によって意図せずリセットされることがあるため注意する。
- 例:
background: whiteはbackground-colorだけでなく、背景に関する他のサブプロパティも既定値へ戻し得る。結果として、別箇所で設定していた背景画像・サイズ等を壊し、事故につながる可能性がある。 - 横方向の中央寄せとして
margin: 0 auto/margin: autoを学んだ人は多いだろうが、これらは「本来触る必要のない上下のマージン」まで0/autoに上書きするのと同等である。- 特に Every Layout で紹介される flow レイアウトとの相性が悪く、flow 側で意図していた
marginを打ち消してしまう。 - 代替として、先述したインライン軸だけを中央寄せする
margin-inline: autoを提案できる。
- 特に Every Layout で紹介される flow レイアウトとの相性が悪く、flow 側で意図していた
- また、
flexのような初期値が別の値にマッピングされるショートハンドにも注意する。「flex: 1だと何が 1 なのか分からないからflex-grow: 1にする」といった発信を見かけたが、flex: 1はflex-basisが 0 に置き換わるので別物である。 - すべてのショートハンドが NG というわけではない。
insetやborderのように「すべての辺に任意の数値を与える」用途では問題になりにくい。- 先述した
backgroundであっても、疑似要素のような設定が差し込まれるのが考えづらい場面でも問題は起こりにくい。 - 指摘対象は、影響範囲が広く、継承や複合的な既定値が絡み、実装上怪しそうな項目に絞る。
- 特に
fontは最悪のプロパティであり、リセット目的以外では使わない方針を伝える。
- ここでいう「デフォルト」とは、CSS の初期値/UA スタイルシートの値/リセット CSS の値/ベース CSS の値を総称したもの。
- 例:
margin: 0や<div>に対するdisplay: blockなどを場当たり的に書いていないかを確認する。意図が薄い場合は設計の一貫性が崩れているサインになりやすい。- 「最初の要素だけ上マージンを打ち消す」目的なら、
:not(:first-child)やフクロウセレクタなどを使い、「最初の要素にだけmarginを適用しない」という設計を提案する。
- 「最初の要素だけ上マージンを打ち消す」目的なら、
- ブレイクポイント下で値をデフォルトへロールバックしたい場合など、デフォルト値を明示する必要がある場合は IMO として
unset/initial/revert/revert-layerを使い、どの次元へ帰りたいのかを明示するよう勧める。- ただし、
displayの CSS 上の初期値は<div>だろうが<p>だろうが一律inlineであるなど、基礎知識が前提になるため、レビュイーの習熟度に応じて指摘の強度は調整する。
- ただし、
:root/bodyに定義されているベースのタイポグラフィは、原則として継承を利用する。局所での再指定は必要最小限に抑える。
- ここでいう「重い指定」はパフォーマンスの重さではなく、影響範囲が大きく、結果として保守性の治安を悪化させやすい指定を指す。
- ベースレイヤーで定義するタイプセレクタの詳細度が高いと、後からの上書きが難しくなり、破綻の原因になりやすい。
- 例として、ベースレイヤーに次のような指定があるケースがある。
a:link, a:visited { color: var(--foreground-link); }
- この指定の問題は詳細度が高い点にある。
a(タイプセレクタ)+:link/:visited(擬似クラス)で詳細度は 0.1.1 になり、class 1つでは上書きできない。- その結果として、リンク色を変えるだけのために、 意味の薄いセレクタの組み合わせで詳細度を上げたり、最悪
!importantを付けるといった対応が増え、治安悪化を招きやすい。
- その結果として、リンク色を変えるだけのために、 意味の薄いセレクタの組み合わせで詳細度を上げたり、最悪
- 新規プロジェクトでは、まずカスケードレイヤー(
@layer)の導入を検討する。- レイヤー設計が明確なら、上記のようなセレクタでも詳細度バトルが起きにくくなる。
- カスケードレイヤーの導入が難しい場合は、ベースのタイプセレクタを原則
:where()で包み、詳細度を 0 に落とすことを推奨する。:where(a:any-link) { color: var(--foreground-link); }
- ただし
:where()には落とし穴もあり、サードパーティ製リセットCSS(例:destyle.css)が:where()を使っておらず、タイプセレクタの詳細度が残っている場合、リセット側が優先されてベースの指定が打ち消される可能性がある。そのため、リセットCSSは可能な限り kiso.css のように:where()によって詳細度が下げられているものを選定する。
display/position/marginなど、レイアウトに関わるプロパティは非常に繊細で、色や文字周りのプロパティと比べて破壊的になりやすい。- 極端な例として、次のような発想は明確に危険だと理解できるだろう。
flexを多用するからと言って、div全部にdisplay: flexを付けるposition: absoluteの相対先の指定が面倒だからと言って、全称セレクタにposition: relativeを付ける
- 上記のような実装はさすがに見かけたことは無いものの、実際には次のような指定が紛れ込むことがある。
aをすべてinline-blockにするpにmax-widthを指定する
- レイアウト系プロパティはコンテキストによって何を定義すべきか or しないかは変化する。ベースのスタイルでレイアウト系の定義が行われている場合、異なるコンテキストでは
unset/revertが頻発して手間が増えたり、意図しない不具合を引き起こすといったリスクが高い。 - レイアウト系プロパティは、リセットCSSで最低限の前提を整えた上で、さらに平坦化が必要な場合にのみ慎重に定義する。ベースでの特異な指定は避けるように伝える。
- 典型例は、リンクやインタラクティブ要素への
outline: none。アウトラインを消すとキーボード操作時にフォーカス位置が見えなくなり、アクセシビリティを阻害するためアンチパターンである。- 近年のUAスタイルシートではフォーカスリングは主に
:focus-visibleのときだけ表示されるため、クリック時に常時アウトラインが出る状況は基本的に起きにくい。
- 近年のUAスタイルシートではフォーカスリングは主に
- 「コーディングしやすいから」という理由で、デフォルトの
line-heightを1にするアプローチも見かけるが、これもアンチパターンである。- デザイン上では1行でも、コンテンツ変動やレスポンシブ・機械翻訳などで改行が発生しうる。フォント次第では行が重なり、読めなくなる可能性がある。
- ベースでは、アクセシビリティの観点から
line-height: 1.5以上を基準に指定し、継承させるように進言する。 - デザイン上どうしても
line-height: 1が必要な箇所があっても、text-box-trimやcalc((1lh - 1em) / 2)でハーフレディングを算出して詰めるなどの代替手段は存在する。
- ダークモード対応が行われていないのに
color-scheme: light darkが設定されていないかも確認する。- 背景色やテキスト色の指定が不足していると、ダークモード時にシステムカラーが適用され、文字色と背景色が同化するなどの事故が起こりうる。
- 海外製のリセットCSSやフレームワーク由来の可能性もあるが、ダークモード非対応なら
color-scheme: light darkは取り除くよう指摘する。 - 暗い背景のサイトであれば、
<meta name="color-scheme" content="dark">を設定するとよい。ユーザーの外観モードに左右されず、スクロールバーなども暗い見た目に寄り、体験の一貫性が上がる。 - 明るい背景のサイトでは、
color-schemeについては特に何も指定しない方針で問題ない。
- 一度リリースしたら終わりの短期LPは別として、一般的なWebサイトは継続的に更新・改修が発生する。とくにCMS案件では、クライアント都合でイレギュラーなコンテンツ差し替えが起こりうる。そうした状況を前提に、defensive(防衛的)なCSSになっているかを確認する。
- CSSを書くときは常に「もし〜だったら?」を自問する。たとえば「テキストが2倍の長さになったら?」「画像が存在しなかったら?」など、想定外のパターンをデザイン・開発の段階から織り込んで考える。
- 固定値の
widthはオーバーフローを起こしやすく、レスポンシブとも相性が悪いので原則避ける。max-widthで目的を満たせるなら、まずそれを採用する。- 「内容の幅に寄せたい」意図があるなら
width: fit-contentを検討する。 - 固定値の
widthを許容するのは、アイコン・チェックボックス・細かな装飾など、グラフィカルな要素に限定する。
- オーバーフローを起こしがちな「幅指定」全般に注意を払う。
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr))は書きがちだが、親要素の幅が360pxを下回るとオーバーフローする。grid-template-columns: repeat(auto-fit, minmax(min(360px, 100%), 1fr))のようにmin()で100%をフォールバックとして与える。min-width: 320pxのような最小幅指定も同様に、320pxを下回るとオーバーフローする。min-width: min(320px, 100%)のようにフォールバックを忘れない。flexではflex-shrink: 0の乱用に注意する。とくに「テキストを変に改行させたくない」という理由で付けられがちなので、まずはmin-width: fit-contentで解決できないか提案する。- ただし、アイコンのように shrink させたくない要素には
flex-shrink: 0が適用されているか確認する。 - なお、flexアイテムの
width: 320pxはflex-basisとして参照されるため、固定値というより「推奨値」として扱われる。そのためflex-shrink: 0が指定されていない限り、min(320px, 100%)のようなフォールバックは不要。 widthとflex-basisの違いは「主軸方向の基準サイズをどちらで持つか」にある。width: 320pxはflex-directionに関係なく「横幅」だが、flex-basis: 320pxはflex-direction: columnでは「高さ」になる。ブレイクポイントでflex-directionを切り替える場合は注意する。- 横幅まわりの優先順位は、
min-width>max-width>flex-basis>widthの順に強い。
- ただし、アイコンのように shrink させたくない要素には
- 固定値の
heightもコンテンツ変動・レスポンシブ変動に弱いので、原則避ける。paddingで意図を再現できるならheight系の指定は不要。- 画面いっぱいのヒーローヘッダーなど、明確に高さが必要な場合は
min-heightを使う。 - 縦横比が決まっているグラフィカル要素や、正方形・正円が望まれるボックスには
aspect-ratioを使い、片側のサイズに追従しつつ比率を維持する。 - これらで意図が再現できないケースに限り、最終手段として
heightを使う。
- 要素数の増減が見込まれる箇所の
flexでは、flex-wrap: wrapが適用されているか確認する。 - 画像に
aspect-ratioやheight: 100%を適用している場合、異なる比率の画像が差し込まれても引き伸ばしにならないよう、object-fit: coverが適用されているか確認する。object-fitの初期値fillが望ましいケースはほぼない。基本はcoverかcontainの二択である。ベースで<img>に一律object-fit: coverを当て、containが必要な箇所だけ上書きする運用も選択肢。
- 改行まわりについて、明確に「指定せざるを得ない理由」がない限り
white-space: nowrapは付けない。また、改行が発生しても可読性が保てるよう、適切なline-heightが設定されているか確認する。- 先述のとおり
line-height: 1は許容しない。 - 改行は可読性・審美性にも影響するため、詳細は別途まとめた内容も参照する。
- 先述のとおり
- 画像の上にテキストを載せるレイアウトでは、画像が読み込まれないと背景とテキストが同化する恐れがある。テキストカラーに対して十分なコントラストを持つ背景色がフォールバックとして用意されているか確認する。
- flex / grid アイテム内で、テキストが長単語だったり、テキスト入力系の
<input>や横スクロールする<table>が含まれる場合、枠を突き抜けてオーバーフローする可能性があるため検証する。min-widthの初期値autoは原則0相当として扱われるが、flex / grid アイテムではmin-contentにマッピングされる(要素の最小サイズ以下には縮ませないルールが働く)ためである。- テキストの折り返し問題は
:rootにoverflow-wrap: anywhereを指定すれば解決できる。kiso.css を採用している場合は指定不要。 - ただし、ブラウザ側で最小サイズが設定されている
<input>や、横スクロールする<table>を含む場合は、依然としてオーバーフローが発生しうる。 - 解決策として、全称セレクタで
min-inline-size: 0を指定するのを提案する。副作用が完全にないとは言い切れないが、デフォルトの挙動で「なぜかオーバーフローする」という直感に反する問題に悩まされるより、「縮みすぎる」という目に見える問題を直すほうが、簡単で合理的である。 - 既存サービスの改修では、この
min-content由来の不具合修正が定期的に発生する。であれば、防衛策としてデフォルトで指定しておくほうがよい。 - Tailwind の作者もこのアプローチを推奨している
- 各コンポーネントは常に「自分以外のことは何も知らない」状態を目指す。
- CSS設計としても、コンポーネントは関係ない場所(例:フッター)へドラッグ&ドロップされても崩れにくいことを目標にする。
- 私がプレイするモンスターハンターシリーズには、看板モンスターとしてリオレウスが存在する。
- リオレウスには原種のほか、亜種/希少種/二つ名個体/歴戦王/ヌシなどの派生種がいる。見た目は色違いに見えても、攻撃パターン/弱点属性/生息地域/ドロップ素材、そして作れる武器・防具まで、実際にはまったく別物である。
- ありがちな失敗は、これらの派生種を「バリエーション」としてまとめ、「リオレウス」という単一ブロックとして定義してしまうことだ。別物を1ブロックで扱うと機能的同一性が保てず、不要な分岐が増えてブロックが複雑化・肥大化し、保守しづらくなる。さらに、希少種やヌシが後から不要になった場合でも捨てにくい。
- Webサイトで例えるならカード類が近い。見た目は似ていても「商品カード」と「記事カード」では役割や内包コンテンツが異なる。これらを「カード」としてまとめると、上記と同じ悲劇が起こりうる。
- このように「見た目」や「大まかなジャンル」だけで共通化せず、機能的にも同一かどうかを判断軸に加える。抽象化は一括変更しやすい反面、不要なものまで一緒に変更されるデメリットにもなる。抽象化はある程度割り切り、将来不要になれば捨てられる設計を目指す。
- プログラミング言語には、同じことを何度も書かないことを良しとするDRY原則があるが、CSSはプログラミング言語ではない。たとえば ECSS のように、あえて重複を許容し、複雑化に歯止めをかける思想もある。
- カードの見た目を共通化したいなら、
--card-backgroundのようなトークンを用意し、それぞれのカードコンポーネントへ適用する。こうしておけば「カード類の背景色をこれに変えてください」といったオーダーにも対応しやすい。
- 「外部関連型レイアウト」は私の造語だが、レイアウト指定には
paddingやgrid-templateのように自身のスタイルだけで完結する「内部関連型レイアウト」と、外部要素との関係で成立する「外部関連型レイアウト」があると考えている。 - 外部関連型レイアウトに該当しうるプロパティの例は以下。
marginは周囲との関係で成立するため外部関連型である。「コンポーネントにmarginを持たせるな」と言われがちな理由もここにある。position: absoluteは相対先の要素があって成立する。加えてinsetも相対先との距離を定義するため外部関連型である。flex/grid-area/justify-self/align-selfなど、親側の指定とセットで成立するプロパティ。width/heightをコンポーネント自身が持つと汎用性が落ち、多くの場合は親要素が制御するのが望ましいため、原則として外部関連型として扱う。min-width/max-width系は文脈次第で性質が変わる。「このコンポーネントは最低この大きさが必要」「これ以上広げてはいけない」など、コンポーネント固有のルールがあるなら内部関連型。一方で「親要素に対して最大50%」のように親との関係で決まるなら外部関連型。subgridは「内部関連型」でありつつ、親がgridであることを前提にする点で「外部関連型」の性質も併せ持つ。subgridを使うカード類は、それを並べるgridを指定した要素も含めてコンポーネントとして扱う。
- ただし例外もある。
- モーダルのように
position: fixedを適用する要素はビューポート基準であり、ビューポートの持ち物であることが明白なので、コンポーネント側に外部関連型レイアウトを持たせることを許容する(ビューポートという大枠から操作しづらい事情もある)。 - グローバルヘッダーのように
position: stickyを適用する要素は、粘着先がビューポートならinsetをコンポーネントに持たせてもよい。一方で、テーブルヘッダーのように特定ボックスを粘着先とするなら持たせない(そもそもテーブルヘッダーをコンポーネント化するケースは稀だが念のため)。 - アイコンやチェックボックスのような小さなグラフィカル要素は、自身が一定サイズを持つ必要があるため内部関連型になる。この場合は
width: 1emなどを指定し、親のfont-sizeで制御できるようにしておくとよい。
- モーダルのように
- 親コンポーネントから子コンポーネントのスタイルを上書きする際、子コンポーネントのセレクタを直接操作するのは避ける。詳細度バトルを招きやすく、子コンポーネントの構造変更に引きずられ、親側の定義修正が必要になるなど副作用が大きい。
- 子コンポーネントの上書き手段には Modifier を追加するなど複数の選択肢があるが、私は子コンポーネントに API 的カスタムプロパティを用意する方法を第一に考えている。
- 例:子コンポーネントの子要素の
border-radiusを12px→24pxにしたい場合- 子コンポーネント側:上書き対象を
border-radius: var(--component-name--radius, 12px)のように置き換える。 - 親コンポーネント側:任意の箇所で
--component-name--radius: 24pxを定義する。 - これにより、継承を利用して安全に子コンポーネントの見た目を差し替えられる。
- 子コンポーネント側:上書き対象を
- API 的カスタムプロパティを使うメリットは次のとおりである。
- カスタムプロパティ経由にすることで 詳細度を分離でき、詳細度バトルを避けられる。
- 親コンポーネントは、子コンポーネント内のどこでAPI 的カスタムプロパティが使われているかを知る必要がない。子コンポーネントの構造が変わっても、親側は基本的に変更不要である。
- 子コンポーネントは、親側のどこでAPI 的カスタムプロパティが定義されるかを知る必要がない。親ルート、ラッパー、あるいは子側の
style属性など、継承で伝達できる場所ならどこでもよい。style属性は優先度が高く避けられがちだが、API 的カスタムプロパティの注入手段として捉えるなら、安全かつ有効に使える。
- Shadow DOM や Scoped CSS でも、継承が有効なら容易に受け渡しできる。
- 将来的にスタイルクエリとも相性が良い。
- API 的カスタムプロパティは、衝突防止と識別性のために
--foregroundのような汎用名ではなく、--component-name--foregroundのようにプレフィックスを付ける方針とする。 - このテクニックは新規案件だけでなく、既存サービスの改修(特に詳細度が破綻している CSS)でも重要なので、覚えておくとよい。
z-indexについては登壇資料を参照してほしい。- コンポーネントルートには 「絶対的な
z-index」のみを指定して、コンポーネント内部の各要素には 「相対的なz-index」のみを指定する。 - 相対的な
z-indexは1または-1以外を許容しない。さらに厳密にするなら、登壇資料の方針どおりカスタムプロパティ経由以外を禁止してもよい。 - 相対的な
z-indexを持つコンポーネントでは、コンポーネントルートにisolation: isolateを指定してスタッキングコンテキストを生成し、z-indexがスコープされていることを確認する。- これが漏れると、他コンポーネントの
z-indexと干渉して意図しない副作用が起こりうる。
- これが漏れると、他コンポーネントの
<dialog>などトップレイヤー要素であっても、現時点ではz-index: calc(infinity)を指定する運用を推奨する。transition-property: overlayは現時点で Chrome のみ対応で、Safari など非対応環境では、閉じるアニメーション中にz-indexの高い要素の下に回り込み、不自然な挙動になる可能性があるため。