CSS: 避けられない悪い部分
(matklad.github.io)- Webページのスタイリングは、シンプルなブログやGUIであれば学べる小さな部分集合で十分だが、ブラウザのデフォルトやレイアウトのような落とし穴が、数日に及ぶデバッグにつながることがある
- 意味のあるHTML5タグを先に使い、ラッパーを減らすと、CSSを既存のマークアップに合わせて機能させやすくなる
- CSSレイアウトには普遍的な単一アルゴリズムがなく、それぞれのシステムが許容する配置方法を理解するアプローチが必要になる
box-sizing,margin,font-size,line-height,word-breakは直感とは異なる動作をし、小さな変更が全体の配置や可読性の問題に発展しうる- シンプルなページでは、CSS reset、classless CSS、flexbox、過剰でないレスポンシブ規則が実用的な出発点になりうる
CSS学習の範囲と基本的な見方
- CSS、HTML、Web APIは非常に広大で専門性を要するが、プログラミングブログやシンプルなGUIのような作業には、現代Webの適度な部分集合で十分である
- 単純な作業に必要な部分集合だけを教える資料は見たことがないが、MDNを追っていけばある程度は把握できる
- 問題は、その存在を予想していなかった落とし穴がページを壊し、原因を見つけるのに数日かかることがある点だ
- このサイトのスタイリングは、約200行の読みやすいCSSで構成されている
良い部分: 意味のあるHTMLとclassless CSS
-
HTML5の意味論的タグ
- MDNのElements Referenceは一通り見る価値があり、HTML要素の数はそこまで多くない
main,article,nav,kbdのようなタグは、ページ構造をより作りやすくしてくれるulは、header > nav内のサイトセクションのように、あらゆる種類の一覧に使えるdetailsは目次に使え、MDNのソースも確認できるdlとdtは対になったリストに使える
-
ラッパーを減らすアプローチ
- 実際のWebサイトのソースを見ると何重ものラッパー要素が多く、レイアウト問題をラッパーで解いているように見えることがある
- 本番CSSの経験についての判断は保留するとしても、意味のあるセマンティックタグだけを使うよう制限し、そのマークアップに合うCSSを探すやり方のほうが理解しやすかった
-
Classless CSS
- スタイルを完全に中立な「何もない」状態へ初期化することはできず、白や透明なテキストも依然としてスタイルである
- 初期化後は、共通のHTML要素を直接スタイリングするやり方が可能になる
main,header,footer,navタグを使えば、多くのCSSセレクタを書かなくてもページ全体のレイアウトを設定できる- この方法ではCSSがHTML構造を仮定することになるが、自分のHTMLとCSSなら、結果が気に入らなければ変えられる
悪い部分: レイアウト、ブラウザのデフォルト、セレクタ
-
レイアウト
- レイアウト問題はWeb特有のものではなく、さまざまなGUIフレームワークでも難しい問題である
- 固定サイズのラスター画像と、それを説明するテキスト段落を画面の長方形の中に配置する方法はいくつもある
- 一般的なGUIは、多くの「レイアウトの自由度」を持つボックスの階層構造である
- 各ボックスのレイアウトは他のすべてのボックスのレイアウトに影響し、通常は余白や重なりなしですべてのボックスがぴったり収まる必要がある
- 普遍的な単一のレイアウトアルゴリズムは存在せず、RectCutからconstraint solvers、その中間領域まで、システムごとに異なるヒューリスティックが使われる
- 「このシステムでレイアウトをどう作るか」よりも、「そのシステムが許容するレイアウトは何か」を考えるほうがよい
-
ブラウザのデフォルトとCSS reset
- CSSのない意味のあるHTMLでも、ブラウザでは色、フォント、サイズ、大きな見出し、下線付きリンクなどのデフォルトスタイルが適用される
- デフォルトスタイルは便利だがブラウザごとに異なり、自分で書いていないデフォルト値に依存すると、別のブラウザでは違う結果が見えることがある
- 一般的な解決策はCSS resetまたは正規化で、CSSの冒頭で明示的な規則によってデフォルト値を上書きする方法である
- デフォルトそのものが悪いのではなく、それらが互いに一貫していないことが問題である
- 実際にどの規則を上書きすべきかは、既存のCSS resetをいくつか比較してみるのがよい
-
Webページはスタイリングすべきか
- Webプラットフォームには、柔軟で適応的な視覚メディアとして捉える見方と、コンテンツ配信に集中し、ユーザーが表現をカスタマイズすべきだという見方が併存している
- デフォルトでは、スタイルのないページは使い勝手が悪く見た目もよくない
- CSSのないページでもそのまま読みやすい世界のほうが望ましいが、現状の環境ではコンテンツにスタイルを適用することが役に立つ
- 上級ユーザーが自分のCSSを持ち込めるようにしておくのがよい
- HTMLマークアップは妥当であるべきで、HTMLをCSSに過剰適合させるべきではなく、ページはreader modeでも機能すべきである
-
CSSセレクタ
- 基本的なCSSは強力な継承のように機能し、Webページの各デザイン要素は複数の規則の影響を受ける
- CSSファイルに追記する形で、既存要素をいつでも「monkey patch」できる
- CSSセレクタは誤った軸で抽象化能力を増やしていると考え、classless CSSやinline styleを使うアプローチもありうる
- Tailwindのようなツールでinline記述の不便さを減らし、JSXや合成を支援するテンプレートエンジンでHTMLの重複を減らせる
- CSS nestingを使えば、遠くまで伸びるセレクタを減らし、コンポーネント単位でスタイリングできる
ボックスモデルと配置: box-sizing, margin, 基本flow, flexbox
-
box-sizing- UIは再帰的な長方形であり、レイアウトは各長方形をどこに置くかを決める過程である
- HTMLのデフォルトでは、要素の
widthとheightは border や padding を含まないため直感的ではない - どこか一か所の padding を増やすと、最初は完璧に見えていた全体レイアウトが思いがけず押し出されることがある
* { box-sizing: border-box; }はCSS resetの1行目にふさわしく、borderの追加を局所的な変更にできる
-
margin collapsing
- 要素の周囲に
8pxの余白が欲しいからといって padding を使うと、隣接する2要素の間隔が16pxになることがある marginは、隣り合う2つのmarginを加算ではなくmaxで結合するように動作する- margin collapsing は非常に便利だが、驚くような挙動を生むことがある
- 子のmarginが親の外にはみ出すことがあると理解しているが、marginに対する直感的理解はまだ十分ではない
- Julia Evansの文章 Moving away from Tailwind, and learning to structure my CSS では、一般に要素自体へmarginを与えるのではなく、親が子同士のmarginを制御する owl selector のやり方が扱われている
sectionの最初の子を除くすべての子にmarginを追加する方法は、margin問題を減らすアイデアとして理解している- こうした知識は、プロのWeb開発者になるか、他のCSSフレームワークをリバースエンジニアリングしないと学びにくい点が不満である
- 要素の周囲に
-
基本flowレイアウト
- 基本レイアウトアルゴリズムは文書言語としてのHTMLの起源と関係しており、テキストと図を中心とした紙の文書生成の用途に合わせて作られているように見える
- 本文テキストに対しては、基本flowは実際に望む動作にかなり近い
- ページ要素の空間配置を直接制御したいなら、基本flowとは別の方法が必要になる
-
flexbox
- flexbox は、一連の要素を縦または横に並べ、利用可能な空間に合わせて適応させるレイアウトである
- 以前は、「これは左、これは右」といった配置にも深いCSS知識や不透明なCSSフレームワークが必要だった
- flexboxはかなり複雑で、MDNを何度も参照する必要があるが、たいていはやりたい作業を終えられる
-
レスポンシブデザイン
- 現代のCSSは画面サイズを問い合わせ、ユーザーエージェントの制約に応じて条件付きロジックを実装できる
- HTMLは本質的にレスポンシブであり、PostScriptやPDFとは異なり、ウィンドウサイズが変わると段落が自動的に再フローする
- 明示的なレスポンシブ規則は避け、レイアウトが妥当に動作するように任せるのがよい
- このブログは明示的な
@mediaクエリなしでも、モバイル、タブレット、デスクトップで十分見やすい - 本文テキストのメインカラムに無条件で
max-widthを設定するだけで十分である
サイズとテキスト: ピクセル、フォント、行の高さ、改行
-
ピクセル
1pxは期待どおりに機能するが、画面上の物理ピクセル1つを意味するわけではない- CSSの
1pxは視角の単位であり、どの画面でも知覚的に似た大きさに見えるよう設計されている - 画面サイズ、ピクセル密度、一般的な視聴距離に応じて、異なる数の物理ピクセルへ変換される
- したがって、異なるディスプレイのピクセル密度を個別に考えなくても、すべてのサイズをピクセル指定できる
- CSSのセンチメートルやインチのような「実際の」単位もピクセル基準で定義されているため、角度のように振る舞う
-
font-sizefont-size: 16pxにおける16pxは特定グリフの大きさではなく、グリフの周囲にある仮想ボックスの大きさである- このボックスはグリフにぴったり一致せず、実際のグリフサイズはフォントによって異なる
font-size-adjustは、フォント間でfont-sizeをより一貫したものにできる- 現状の
font-size-adjustはかなりニッチな機能に見え、個人的にはbox-sizingの横にfont-size-adjust: ex-height 0.53;を置きたいが、そうしているページは少ない font-sizeのデフォルト値はブラウザ間で比較的一貫しており、16pxが圧倒的な標準である- フォントによっては
16pxが小さく見えることがあり、一部のデフォルトフォントは特に小さい - Appleでは
font-family: serifがsans-serifよりかなり小さく見え、16pxではほとんど読みにくい - CSSで
font-sizeを設定すると、ブラウザのデフォルトフォントサイズ変更の仕組みを無効化してしまう - テキストがデフォルトで読みやすいと仮定せず、別の設定でも確認すべきである
font-size-adjustで自由度を減らしfont-sizeの意味を固定したうえで、16pxのデフォルトフォントサイズで問題なければ完了である- そうでなければ
font-sizeをもっと大きな値に設定し、その後 reader mode でも読みやすいか確認すべきである
-
line-height- 名前に反して、
line-heightは1行の高さを設定するものではない line-heightは、同じフォントで設定されたグリフのまとまりの高さである- すべてのテキストが同じフォントなら、行の高さとグリフのまとまりの高さは一致する
- 一部の単語が
monospaceフォントに設定されると、予想と異なる結果が起きうる font-size-adjustはボックス内のグリフサイズは補正するが、相対位置までは指定しない- 異なるフォントのテキストまとまりが baseline を共有するよう垂直整列されると、それぞれの line-height line-box が互いにずれる
- 行全体の高さは合成された結果として予想より大きくなることがある
- この効果は Deep dive CSS: font metrics, line-height and vertical-align で詳しく扱われている
- 名前に反して、
-
vertical rhythm
- vertical rhythm とは、見出しや画像があっても段落間の行位置が同じ相対位置に来るよう揃える考え方である
- Webページの裏に見えない罫線ノートがあるかのように合わせる方法として説明される
- 単一カラムレイアウトでは有用ではないと判断している
- 2カラムレイアウトでは左右の行を揃えたくなるかもしれない
- 単一カラムでそのために複雑な努力を払うのは理にかなわない
-
word-break- flowレイアウトの利点は、ウィンドウが狭くなるとテキストが行としてきれいに分割される動的な振る舞いである
- 行は空白やハイフン挿入可能位置でしか分割できない
inline codeやURLのような長い区間は分割されないことがある- この問題はモバイル機器で横方向のオーバーフローを引き起こし、通常は公開後になって初めて気づく
- これを直す単一の決め手はないが、Against Horizontal Scroll にいくつかのヒントがある
実用的な結論
- シンプルなブログを作るには、HTMLとCSSの必要十分な部分だけを短く説明する資料が必要である
- margin collapsing のような問題に打ちのめされず、シンプルなブログを作れる程度のHTMLとCSSを説明する100ページほどの短い本がほしい、という要望で締めくくられている
1件のコメント
Lobste.rs の意見
細かい指摘ではあるけれど、レスポンシブデザイン の定義は「さまざまなデバイスやウィンドウ/画面サイズで適切にレンダリングされ、使いやすさと満足度を保証すること」。
メディアクエリやコンテナクエリはそれを実現するための道具の一つにすぎず、レスポンシブデザインは「何にでもメディアクエリを使おう」というより考え方に近い。
だから「悪いもの」はレスポンシブデザイン自体ではなく、メディアクエリやブレークポイントの乱用だと見るのが適切だと思う
「Browser defaults」の節はかなり誤解を招く。リセットスタイルシートと正規化スタイルシートは目的も動作も大きく異なるのに、記事ではそれを混同している。
リセットはブラウザ間の差をなくすというより、要素ごとの既定の差を消して
olのpadding-inline-startやbuttonの既定の見た目のようなものを取り除き、ユーザーエージェントスタイルシートの上ではなく白紙の状態からスタイルを作れるようにする。正規化はユーザーエージェントスタイルシートと協調しようとするアプローチで、ブラウザ間の差を揃える部分と、「より合理的」だと考える既定値に変更する部分が混在している。
最近ではブラウザの既定値が意味のある形で異なるケースはあまり多くないので、一般的なWebコンテンツの作者ならほぼ無視してよい。例外としては Chromium has the wrong
tableborder-color、WebKit fixed theirs 1½ years ago、一部フォーム要素の余白/サイズ差、appearance、WebKit の<summary>::markerスタイリングあたりが残っている。box-sizing: border-boxにも反対寄りだ。border-boxはレイアウト中心で、content-boxはコンテンツ中心なので、親がレイアウトを担い、コンテンツ基準で設計するほうがよいと思う。比率を扱うときはcontent-boxが必要で、border-boxが実際に有用なのはbodyをビューポートいっぱいにする程度。font-size-adjustについても懐疑的だ。広く知られた問題を、あまり検証されていない別の問題に置き換えるだけで、ある人には少し良くなり、別の人には少し悪くなるかもしれない。根本問題は解決できず、ユーザーのフォント比率やメトリクスについて根拠のない仮定を置くことになる。line-heightと「同じフォントで設定する」という表現も厳密ではない。実際にはフォントサイズ、言語切り替え、font-family: monospace、vertical-align、<sup>、<sub>、フォントメトリクスが絡み合っていて、単純に同じフォントの問題とは言いにくい。マージンの相殺は記事で書かれているより複雑だが、かなり実用的でもある。一般的なコンテンツに
flexやgridを使うとgapやマージンをずっと調整し続けることになり、壊れやすくなりがちだ。display: flow-rootを使えば、子のマージンが親のマージンと相殺されるのを防げる。レスポンシブデザインについての大枠には同意しないが、不必要なメディアクエリを減らし、ブラウザと戦わない方向性は正しい。コンテンツ基準でレイアウトの変化を表現できるなら、たいていはそのほうが良い。
最近はビューポート単位を使った clamp 線形補間 をよく使っている:
margin-inline: --vw-lerp(1rem at 20rem, 2.5rem at 60rem);をmargin-inline: clamp(1rem,1rem + ((2.5 - 1)/(60 - 20)*(100vw - 20rem)),2.5rem);に展開する形。昨年これを implemented this as a LightningCSS visitor として実装したが、ビューポートが 20rem 以下なら 1rem、60rem で 2.5rem まで滑らかに増加してから止まるので、ブレークポイントなしでユーザーのフォントサイズにも対応でき、感触が良い。
font-size-adjustが本当にそう動くのかはよく分からない。名前のせいで紛らわしいけれど、font-sizeは em ボックスの大きさを、font-size-adjustは em ボックス内のグリフサイズを変えるものだと理解している。だから
emとremはそのままで、chは変わる。ただしchはもともとフォント依存なので、変わるのは自然だ。font-size-adjustについて記事を書いてほしい。専門家ではないので確信は薄いけれど、今のところほとんど知られていないのにものすごい改善に見える。どんな2つのフォントでもすべての文脈で自動的に合わせられるわけではないが、em ボックスではなくxの大きさを合わせるだけでも、フォント/文脈の 90% はカバーできると思う記事の趣旨は良いし、HTML/CSS に完全に没入していない人たちの視点も重要だが、「悪いもの」のかなりの部分は文脈次第で良くもなりうる。
CSS セレクタは乱用しやすいが、本質的に悪いわけではない。A か B かと結論づけるより、一般ルール/セレクタを置きつつ、必要なときに例外ベースのクラスやユーティリティクラスを振ることもできる。
記事の中にも良いセレクタの例がある:
メディアクエリも、container queries で解決できるなら不要かもしれない。
CSS は表面的には非常に広大に感じられるが、広く使われ、できることも多いだけに、意見の衝突も起こりやすい。それでも CSS がどれだけ進化してきたか、そして概念を受け入れれば比較的少ないコードで何ができるかも見るべきだ。
もっと学びたいなら https://every-layout.dev/ は、CSS で複数の要素がどう噛み合うかを理解するのにかなり役立った
Web レイアウトが分かり始めたきっかけは、良いWebサイトは基本的に縦方向の設計だと気づいたときだった。要素は基本的に上下に積み重なるべきで、まずモバイル画面向けに設計し、大きな画面ではおまけとして広げればよい
この主張には同意しにくい。CSSネストは単なるシンタックスシュガーにすぎず、過度に具体的なセレクタの問題を意味のある形で回避できるわけではない。
15年前にも Sass でセレクタのネストを多用していたが、結局は CSS セレクタを HTML 構造にあまりにも強く結びつけ、自分で自分の首を絞めるという結論に一つずつ行き着いた。
ネストの罠はプロジェクト初期にはあまり表面化しない。新機能を主に作るグリーンフィールド段階では、こういう書き方の CSS はとても良さそうに見える。
数か月後に大きなレイアウト修正とデザイン刷新を始めると、HTML のラッパー要素の位置が入れ替わり、それに合わせて CSS を調整する作業は、LSD をキメて ルービックキューブを解いているような感覚になる。
セレクタの詳細度管理は、ほとんどを単純セレクタ、つまり単一クラスで低く保ち、ごく少数の複合セレクタと
a:hoverのような結合セレクタだけを使う方式が頂点だったと思う。BEM や OOCSS のような系統で、その後は関心が JS 中心のツールへ急速に移っていった。興味深い記事だが、筆者はネストしたセレクタをまったく効果のない位置で使っているように見える。
&を省略できるようにしたのは間違いで、常に&を使うべきだと考える人もいる。かなりもっともな立場だと思う。個人的にはまだ半々だ。最初は間違いだと思っていたが、スタイルを大量に書いたあとで
header { … }で囲むだけでスコープを狭められる場面ではかなり便利だ。@keyframesのような、セレクタベースではない at ルールもその中に書けると良いと思う。これは正直かなりひどい助言だ。Maklad の記事は好きだが、これは仕事として CSS を書いたことがない人が書いたのは明らかに見える。
ほぼ全部が、プロの CSS 設計では避けるようなアマチュア的な悪い慣行だ。
セレクタなしでそれをスタイリングしているうちに、
<main>や<nav>もセレクタなしで装飾するようになる。一方、プロの現場ではコンテンツボックスを設計する時間はごくわずかだ。プロジェクト初期に一度作り、その後は小さなバグを少しずつ直す程度である。
大半の時間はカスタムコンポーネントや再利用コンポーネントを作ることに費やされる。再利用コンポーネントのほうが難しく、実質的にはサイト専用の Bootstrap クローンを作ることになる。
カスタムコンポーネントは簡単だがコード量が増え、他のコンポーネントと意図せず干渉しないように、BEM、OOCSS、Tailwind のようなユーティリティクラスなどの戦略が必要になる。
要するに、手法ごとに適した規模が異なるということだ。プロ向けの CSS 設計手法が役に立たないように見えるなら、おそらく別の規模の問題を解いているのだろう。
それでも
Bad: Wrappersには同意する。サイト全体を 1〜2 ファイルで書く CSS の達人も見たことがあるし、CSS を大量に書く人たちも見てきた。後者の道は、結局は大量の CSS を管理するために BEM のような誤ったアプローチへ進みがちだ。
記事の中には互いに矛盾する助言があるように見える。
Good: Classless CSSとBad: CSS selectorsが並んでいるが、クラスレス CSS を使うなら、むしろ CSS セレクタへの依存は強くなる。参考: https://www.keithcirkel.co.uk/css-classes-considered-harmful/
「見えない罫線入りノートが Web ページの背後にあるかのような」vertical rhythm は、EM 値を使えば十分実現できる。
異なるフォントを混在させるとメトリクスの差で少し揺れることはあるが、その場合でも flex の
align-items: baselineを使える。