GoogleのModern Web Guidanceに学ぶ、モダンCSSのDos / Don'ts大全

Googleがリリースした「Modern Web Guidance」スキルを使うと、AIエージェントが最新のWeb機能でコードを書けるようになります。
スキルが参照するガイドラインはMarkdownで書かれており、Googleが推奨するWeb開発のDos / Don'ts、つまり「やるべきこと・やってはいけないこと」がまとまっています。Googleお墨付きの、モダンなフロントエンド開発の知見が詰まっています。AIエージェントを使う・使わないにかかわらず、ウェブ技術の勉強に使ったり、実装に迷ったときに「Googleはどう言っているのかな?」の判断基準にしたりできます。
たとえばCSSのレイアウトのドキュメントは次のURLで確認できます。
本記事は、Google Chromeチームが公開するModern Web Guidance(Apache License 2.0)の各ガイドをもとに、次の方針でまとめました。
- 複数のガイドに散らばったCSSの指針を横断し、現時点で全ブラウザ対応した機能だけを抽出
- Dos / Don'tsがはっきり示されている項目に絞って再構成
- 原文の訳に加えて、初心者向けの補足・最新のブラウザ対応状況・関連する筆者の記事や登壇資料を添えた
基礎方針(Foundations)
重複を避け、変数より組み込みの仕組みを優先する
同じ値をCSSのあちこちに書くと変更時に直し漏れが起きます。まず変数(カスタムプロパティ)で重複を避けます。さらにブラウザ標準の仕組みで表現できるなら変数より優先します。
■ やるべきこと(Dos)
- 色をそろえたいときは変数ではなく
currentColorを使う - 親子で同じ値を使いたいときは変数ではなく
inheritキーワードを使う -
font-size: var(--size)の繰り返しではなくem単位を使う - ボックスモデルの値の繰り返しではなくコンテナ単位の
cqw/cqh(論理版はcqi/cqb)を使う
物理プロパティより論理プロパティを優先する
書字方向が変わっても破綻しないよう、物理プロパティより論理プロパティを優先します。論理プロパティは書字方向に応じて向きが変わるプロパティです。RTL(右横書き)だけでなく、日本語の縦書き(writing-mode: vertical-rl)でも向きが自動で切り替わります。横書きのみのサイトでは見た目は変わりませんが、外部の翻訳ツールが訳文を差し込む場合などの保険になります。
■ やるべきこと(Dos)
-
margin-leftではなくmargin-inline-startのように論理プロパティを使う
/* RTL では自動的に右側の余白として扱われる */
.item {
margin-inline-start: 16px;
}
■ やってはいけないこと(Don'ts)
- 論理プロパティを無条件に使わない。「これはRTLで反転してほしいか」と自問し、答えが「いいえ」なら物理プロパティを使う。たとえば言語に関係なく常に同じ向きに出したい影やアイコンの位置などは、物理プロパティのままにする
レイアウトの基礎(Fundamentals)
レイアウトはブラウザのレイアウトエンジンに任せるほど速くなります。固定の幅や高さ、込み入ったメディアクエリに頼る前に、intrinsic sizing・論理プロパティ・aspect-ratioを先に検討します。intrinsic sizingとは、要素の中身に応じてサイズを決める仕組みです。
どのレイアウトを使うかの判断基準
どのレイアウト(FlexboxやGrid等)を使うかは、上から順に見て最初に当てはまったもので決めます。
| 用途 | 使うもの |
|---|---|
| 要素を1方向(行か列)に並べるだけ | Flexbox(1次元・コンテンツ主導) |
| 入れ子の要素を親グリッドのトラックに揃えたい | subgrid(2次元・親トラックを継承) |
| 行と列の両方を持つ複雑なページ・コンポーネント構造 | Grid(2次元・骨格を先に定義) |
| 長い文章を新聞のような段組みに分けたい | multi-column(1次元のフロー) |
| 高さがバラバラな要素を隙間なく詰めたい |
grid-auto-flow: denseのGridで代用する |
| 要素をページの上に浮かせ、DOMやスタッキングコンテキスト(重なり順を決めるまとまり)を越えてトリガーに追従させたい | anchor positioning(トリガーにanchor-name、オーバーレイにposition-anchor) |
固定値よりintrinsic sizingと論理プロパティを使う
サイズや余白は、できるだけ固定のwidth/heightではなくintrinsic sizingと伸縮するトラックで指定します。メディアクエリが減り、壊れにくいレイアウトになります。
■ やるべきこと(Dos)
- レイアウトの寸法や余白には論理プロパティ(
inline-size・block-size・margin-inline・padding-block・inset-inline-start)を使う - コンテンツ主導ならFlexbox、骨格を先に決めるならGridという考え方で選ぶ
- 両軸の揃えは
place-content・place-items・place-selfの一括指定でまとめる - 固定値の前にintrinsic sizing(
min-content・max-content・fit-content())と伸縮するトラック(fr・minmax())を使う - メディア要素には
aspect-ratioで先に場所を確保し、読み込み前のレイアウトシフトを防ぐ
.sidebar { inline-size: max-content; } /* 折り返せない最長の語に合わせる */
.main-content { inline-size: fit-content; } /* 使える幅まで伸び、それ以上は伸びない */
.media { aspect-ratio: 16 / 9; inline-size: 100%; block-size: auto; }
body.centered { display: grid; place-content: center; min-block-size: 100dvb; }
Flexbox
Flexboxは1次元のレイアウトです。要素は主軸(main)に沿って流れ、交差軸(cross)で揃えます。ナビゲーション・ツールバー・項目の横並びなど、1方向の並びに向きます。
1方向の並びはFlexboxを使う
display: flexで文脈を作り、flex-direction(デフォルトはrow)で主軸を決めます。
■ やるべきこと(Dos)
- 折り返しの可能性があれば
flex-wrap: wrapを付ける。nowrapのままoverflow: auto/hiddenもないと、狭い画面ではみ出す - 子要素の伸縮は
flex-grow/flex-shrink/flex-basisを個別に書かず、flex一括指定(例:flex: 1 1 250px)で書く - 要素間の余白は子の
marginではなくgap(またはrow-gap/column-gap)で取る - 位置揃えのキーワードには
safeを付ける(例:align-items: safe center)。コンテナが中身より狭くてもフォーカス対象が見切れない - 1つの要素だけ主軸の端に寄せたいときは
margin-inline-start: auto(またはmargin-block-start: auto)を使う。これが定石 - 要素ごとの交差軸の揃えは
align-selfで上書きする - 全要素を交差軸で中央寄せするなら
align-items。1つの要素を両軸で中央に置くならmargin: auto。align-contentは折り返して行に余りがあるときだけ使う - URLやコードなど折り返せない長い中身を持つflex要素には
min-inline-size: 0(またはmin-width: 0)を付ける。flex要素はデフォルトで中身より小さくならず、はみ出すため
.card-grid { display: flex; flex-flow: row wrap; gap: 1rem; }
.card-item { flex: 1 1 250px; } /* 伸び・縮み・基準サイズ */
.card-item-action { margin-inline-start: auto; } /* 主軸の端へ寄せる */
.toolbar { display: flex; align-items: safe center; }
■ やってはいけないこと(Don'ts)
- flex要素に
justify-selfを使わない。Grid・block・絶対配置でしか効かない。代わりにautoマージンを使う - 操作できるコンテンツの並べ替えに
orderやflex-direction: *-reverseを使わない。見た目の順序だけが変わり、DOM順は変わらない。キーボードのフォーカス順が見た目と食い違う -
space-around(両端は半分の余白)とspace-evenly(前・間・後ろが等間隔)を混同しない - 軸の入れ替わりを忘れない。
flex-direction: columnのとき、justify-contentはブロック軸、align-itemsはインライン軸を揃える。デフォルトとは逆 - コンテナと子を互いに埋め合うサイズにしない。はみ出しや予想外の結果につながる。どちらか一方に確定したサイズを与える
- 同じ要素に
flex-basisとwidth/inline-sizeを両方指定しない。flex文脈ではflex-basisが優先されwidthは無視される
/* どちらも避ける書き方 */
.flex-item {
justify-self: end; /* flex では効かない。auto マージンを使う */
flex-basis: 200px;
width: 300px; /* flex-basis が優先され、これは無視される */
}
グリッドとサブグリッド(Grid and subgrid)
Gridは2次元のレイアウトです。行と列を明示的に定義するか、ブラウザに推測させます。subgridは、入れ子のグリッドが親のグリッド線をそのまま使える機能です。これで子孫要素を兄弟どうしで揃えられます。
行と列を持つ構造は Grid を使う
■ 列数が決まっているか
- 決まっている → 明示的なトラック(
grid-template-columns: 200px 1fr、repeat(3, 1fr)など)を使う- 列ごとにサイズが違う(サイドバー+メイン、全幅のヘッダーなど) →
grid-template-areasで名前付きの読みやすい領域にする - 全列が同じ、または線番号だけで配置する →
repeat(N, ...)や名前付き線を使う
- 列ごとにサイズが違う(サイドバー+メイン、全幅のヘッダーなど) →
- 決まっていない(レスポンシブ、項目数不明) →
repeat(auto-fit, minmax(min, 1fr))を使う- 最終行の項目を伸ばして余白を埋めたい →
auto-fit - 空の最終行トラックを最小サイズで残したい →
auto-fill
- 最終行の項目を伸ばして余白を埋めたい →
■ 特定の場所に置くか
- 置く →
grid-column: <start> / <end>かgrid-area: <name> - ただ複数トラックにまたがるだけ(位置は問わない) →
grid-column: span <n>
■ 子が親のトラックサイズを継承するか(兄弟どうしで端を揃えるか)
- する → 該当する軸にsubgridを使う
- セルごとの子の数が可変 → 片方の軸だけsubgrid。もう一方は
grid-auto-rows/grid-auto-columns - 子の数が固定 → 両軸のsubgridでよい
- セルごとの子の数が可変 → 片方の軸だけsubgrid。もう一方は
- しない → 通常のGridでよい
■ やるべきこと(Dos)
- ページ全体の複雑なレイアウトには
grid-template-areasを使う。領域名がそのまま説明になり、宣言を行と列に揃えて書くと一目で構造が分かる - レスポンシブなカードグリッドには
repeat(auto-fit, minmax(200px, 1fr))。空トラックを最小サイズで残すならauto-fill - 比率で分配するなら
fr、伸縮するが上限のあるトラックにはminmax(min, max) - 配置は
grid-column: span <n>(トラックをまたぐ)・grid-column: <start> / <end>(特定の線に置く)・grid-area: <name>(名前付き領域)で指定する - カードリストの「端がそろわない」問題はsubgrid(
grid-template-columns: subgridかgrid-template-rows: subgrid)で解決する。たとえばブログカードのタイトル・メタ情報・CTAが兄弟どうしで揃う - subgrid宣言の直前に
grid-template-rows/-columnsの明示宣言を置く。同じカスケード内で古いブラウザのフォールバックになる
■ やってはいけないこと(Don'ts)
-
auto-fit/auto-fillのトラックサイズが項目の中身から決まると思わない。repeat()のサイズ引数から決まる - 操作できるコンテンツに
grid-auto-flow: denseを使わない。詰め込みは効率的だが見た目の順序が変わり、DOM順のキーボードフォーカスが崩れる - 子の数が可変のとき両軸にsubgridを使わない。余りは最終トラックに入る。暗黙の軸は
grid-auto-rows/grid-auto-columnsに任せる -
justify-items/align-items(トラック内で項目の中身を揃える)とjustify-content/align-content(コンテナ内でトラックを揃える)を混同しない。取り違えても何も起きず、原因に気づきにくい - コンテナに確定した
inline-sizeがないままrepeat(auto-fit/auto-fill, ...)を使わない。display: inline-gridやサイズ未指定のflex要素の中では分割する幅がなく、トラック数が安定しない
/* inline-grid では分割する幅がなく、トラック数が安定しない */
.grid {
display: inline-grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
コンテナクエリ(Container queries)
コンテナクエリは、画面幅ではなく親要素(コンテナ)の幅に応じてスタイルを変える仕組みです。コンテナクエリはコンポーネントの文脈、メディアクエリはページ全体のレイアウトやユーザー設定(prefers-color-scheme・prefers-reduced-motion)に使う、と覚えます。
親要素の幅に応じた切り替えはコンテナクエリを使う
子孫をクエリする前に、ラッパーに抑制(containment。中身のサイズ計算を外と切り離す指定)の文脈を作ります。幅だけならcontainer-type: inline-size、両軸ならcontainer-type: sizeです。
■ やるべきこと(Dos)
- ラッパーに
container-type: inline-size(幅のみ)かcontainer-type: size(両軸)を付けて抑制の文脈を作る - 入れ子の文脈が衝突しそうなら
container-name(または一括指定container: inline-size card)で名前を付ける - 流動的なフォントサイズや余白の計算にはコンテナクエリ単位を使う。
cqi/cqb(論理のインライン/ブロック)・cqw/cqh(物理)・cqmin/cqmax -
container-type: sizeを使うときはコンテナに確定したblock-sizeを与える。ないと抑制により中身が無視され、子孫がつぶれる
.card-wrapper {
container: inline-size / card; /* container-type + container-name の一括指定 */
}
@container card (inline-size > 400px) {
.content {
display: flex;
gap: 2rem;
}
}
.title {
/* ビューポートでなくコンテナ幅に連動した流動的なフォントサイズ */
font-size: clamp(1rem, 4cqi, 2rem);
}
■ やってはいけないこと(Don'ts)
-
container-typeの値にblock-sizeを使わない。無効。両軸はsize
/* block-size は container-type の値として無効 */
.wrapper {
container-type: block-size;
}
-
container-typeを宣言した後で子のintrinsic sizeがコンテナに影響すると思わない。抑制が効くと、コンテナは子要素が無いものとして扱われる - 条件を満たさない祖先の子孫でコンテナクエリ単位に頼らない。小ビューポート(
svw/svh)にフォールバックする
ネイティブのオーバーレイとアンカーポジショニング(Native overlays & anchor positioning)
オーバーレイの基本要素は用途で使い分けます。<dialog>・popover・anchor positioningはいずれも主要ブラウザで使えるようになりました。
オーバーレイは用途で使い分ける
■ やるべきこと(Dos)
- 一時的で非モーダルなUI(ポップアップ・トースト・ツールチップ)には
popoverを使う。トップレイヤー(ページ最前面の特別な層)に乗るためz-indexの管理が要らない - フォーカストラップと操作不可の背景が必要なモーダルには
<dialog>を.showModal()で開く
■ やってはいけないこと(Don'ts)
- 同じ要素に
popoverと.showModal()を併用しない。実行時には排他的で、両立しない
アンカーポジショニングは機能検出してフォールバックを用意する
anchor positioningは、トリガー要素にオーバーレイを空間的に紐付けて配置する機能です。DOMやスタッキングコンテキストを越えても追従します。主要ブラウザで使えるようになりました。古いバージョンも考慮するなら、機能検出とフォールバックを用意しておくと安全です。
■ やるべきこと(Dos)
- 配置とサイズは
position-area(またはインセットにanchor())とanchor-size()でトリガー基準に指定する - オーバーレイがビューポートをはみ出すときは
position-try-fallbacks: flip-block(またはflip-inline)でブラウザに再配置させる -
@supports (anchor-name: --x)で機能検出し、絶対配置のフォールバックを用意する
■ やってはいけないこと(Don'ts)
- 1つの
position-areaの値に物理キーワードと論理キーワードを混ぜない。座標系はどちらか一方に統一する
原典: Modern Web Guidance / CSS Layout — 5. Native overlays, anchor positioning & stacking contexts
オーバーフローとレイアウトの安定(Overflow tracking and layout stability)
スクロール、はみ出し、切り抜きを予測しやすく扱います。
スクロールバーはoverflow: autoで必要なときだけ出す
■ やるべきこと(Dos)
- スクロールバーは、中身が実際にはみ出したときだけ出す。
overflow: autoを使う
.box {
overflow: auto;
}
■ やってはいけないこと(Don'ts)
-
overflow: scrollを避ける。スクロールするものが無くてもスクロールバーを強制表示する
.box {
overflow: scroll;
}
切り抜きだけしたいならoverflow: clipを使う
■ やるべきこと(Dos)
- スクロールコンテナを作らずに中身を切り抜くだけなら
overflow: clip。あえてはみ出させたい分はoverflow-clip-marginで許可する
■ やってはいけないこと(Don'ts)
- 切り抜きたいだけのときに
overflow: hiddenを使わない。hiddenはスクロールコンテナを作り、プログラムからスクロールできてしまう
/* 切り抜きたいだけなら clip。hidden はスクロールコンテナを作る */
.box {
overflow: hidden;
}
scrollbar-gutter: stableでスクロールバーの領域を確保する
■ やるべきこと(Dos)
- 中身が増えたときのレイアウトシフトを防ぐため、
scrollbar-gutter: stableでスクロールバーの領域をあらかじめ確保する。スクロールバーが出た瞬間に中身の幅が変わるのを防げる
スクロールの連鎖をoverscroll-behaviorで止める
■ やるべきこと(Dos)
- スクロール可能なコンテナでは
overscroll-behavior: contain(またはnone)を使う。コンテナの端までスクロールしたとき、その先のスクロールが親やページ本体へ伝わるのを止められる
複数行の省略は-webkit-line-clampの三点セットを使う
■ やるべきこと(Dos)
- 複数行のテキストを「…」で省略するには
-webkit-line-clamp・display: -webkit-box・-webkit-box-orient: verticalの3つを組み合わせる。-webkit-接頭辞が付くが、この書き方は仕様で正式に定義済みで非推奨ではない - 三点セットと並べて
line-clampショートハンドも書いておく。未対応のブラウザは無視するだけで、将来対応したブラウザでそのまま効く
.scrollable-list {
max-block-size: 400px;
overflow-y: auto;
scrollbar-gutter: stable; /* スクロールバーの領域を確保する */
overscroll-behavior: contain; /* ページ本体へスクロールを連鎖させない */
}
.snippet {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
line-clamp: 3; /* 未対応のブラウザでは無視される */
overflow: clip;
}
原典: Modern Web Guidance / CSS Layout — 6. Overflow tracking and layout stability
ビューポートの扱い(Viewport mechanics)
モバイルの全画面コンテナにはdvh・dvwを使う
■ やるべきこと(Dos)
- ブラウザUIの伸縮に追従させたいコンテナには
dvhやdvwを使う。スマホではスクロールに応じてアドレスバーが伸縮し、その分だけ表示領域の高さが変わる。dvhは、ブラウザUIの伸縮を反映した「動的な」ビューポートの高さの単位
全幅レイアウトに100vwを使わない
■ やるべきこと(Dos)
- 全幅のレイアウトには
100%・100dvw・100svwを使う
.full-width {
width: 100%; /* もしくは 100dvw / 100svw */
}
■ やってはいけないこと(Don'ts)
- 全幅のレイアウトに
100vwを使わない。100vwはスクロールバーの幅を含まず、横方向のはみ出しが起きる
.full-width {
width: 100vw;
}
原典: Modern Web Guidance / CSS Layout — 7. Viewport mechanics and track distribution
継承とカスケード
カスケード(cascade)は、複数のスタイルが競合したときにどれを適用するかを決める仕組みです。継承(inheritance)は、親要素の値が子要素に引き継がれる仕組みです。
詳細度はカスケードレイヤーと:where()で管理する
■ やるべきこと(Dos)
- 詳細度の管理にはカスケードレイヤー(
@layer)や:where()を使う。カスケードの挙動が予測しやすくなり、書き手の意図どおりに振る舞う
■ やってはいけないこと(Don'ts)
- 詳細度を管理するためにBEMなどの命名規則を導入しない
カスケードレイヤーで優先順位を明示する
@layerで優先順位のゾーン(reset・base・theme・components・utilitiesなど)を定義します。順序は先頭でまとめて宣言します。
■ やるべきこと(Dos)
- レイヤー順を先頭でまとめて宣言する
/* 後に書いたレイヤーほど優先される */
@layer reset, base, theme, components, utilities;
- 各レイヤーの内部では
:where()を使う。:where()は詳細度を0にして包む機能で、意味のあるシグナルだけで競合し、簡単に上書きできるデフォルト値を作れる
明示的な値ではなくグローバルキーワードで意図を表す
具体的な値ではなくinherit・initial・unset・revertを使うと意図が明確になります。
■ やるべきこと(Dos)
- 親と同じトランジションを子に持たせたいときは、書き直さず
transition: inheritを使う - プロパティを初期値に戻したいときは値を書かず
initialを使う
.child {
/* 親と同じトランジションをそのまま受け継ぐ */
transition: inherit;
}
原典: Modern Web Guidance / CSS — 2. Inheritance and the cascade
セレクタとスコープ(Selectors and scoping)
ブラウザ標準のモダンなセレクタを使えば、プリプロセッサやJavaScriptによる複雑な状態管理を減らせます。
子の状態で親をスタイルするには:has()を使う
子要素の状態に応じた親のスタイルは、JavaScriptでクラスを付け外しせずCSSだけで表現できます。label.has-checkedのようなクラスを手で管理する代わりにlabel:has(:checked)と書けます。
■ やるべきこと(Dos)
- 子の状態で親を狙うときは
:has()を使う。状態管理がCSSに寄り、JavaScriptが減って表示と状態のズレも起きない
/* チェックされた子を持つ label を狙う */
label:has(:checked) {
background: var(--color-accent);
}
■ やってはいけないこと(Don'ts)
- 親の見た目を変えるためにJavaScriptでクラスを付け外ししない
// チェック状態が変わるたびにクラスを付け外しする
checkbox.addEventListener("change", () => {
label.classList.toggle("has-checked", checkbox.checked);
});
-
:has()を入れ子にしたり、内部で疑似要素を使ったりしない(ブラウザAPIの制限)
n番目の特定要素を狙うには:nth-child(... of ...)を使う
ある条件を満たす要素のうちn番目をスタイルしたいときは:nth-child(<An+B> of <selector>)を使います。
■ やるべきこと(Dos)
- 条件付きのn番目には
:nth-child(... of ...)を使う
/* 見つかった中で最初の「開いている」details を狙う */
details:nth-child(1 of [open]) {
outline: 2px solid var(--color-accent);
}
■ やってはいけないこと(Don'ts)
- 同じ意図で
details[open]:first-childと書かない。これは「最初の子であり、かつそれが開いている場合」にだけ当たり、意図が変わる
/* 最初の子が開いているときだけ当たる。意図がずれる */
details[open]:first-child {
outline: 2px solid var(--color-accent);
}
フォールバックにはルール複製ではなく:is()/:where()を使う
サポートされていないかもしれない疑似クラスのフォールバックは、CSSルールを複製せず:is()や:where()の寛容なパース(forgiving parsing。対応していないセレクタが交じっても全体を無効にしない解釈)で1つにまとめます。
■ やるべきこと(Dos)
- フォールバックは
:where()(または:is())で1つのルールにまとめる。これらは未対応のセレクタが交じってもルール全体を無効にしないため、複製せずに済む
[popover]:where(:popover-open, .\:popover-open) {
/* 同じスタイルを1つのルールで */
}
.\:popover-openは、ポリフィルが付けるコロン入りのクラス名:popover-openを狙うセレクタです。クラス名に含まれるコロンは、疑似クラスと区別するため\でエスケープします。
■ やってはいけないこと(Don'ts)
- 疑似クラスのフォールバックのためにCSSルールを複製しない
/* `:where()` を使わずにルールを重複させている */
[popover]:popover-open {
/* ネイティブ popover 向けのスタイル */
}
[popover].\:popover-open {
/* ポリフィル版のために同じスタイルをもう一度 */
}
- 疑似要素にこの手法を使わない。
:is()/:where()は疑似要素に対応していない
無関係な状態・対象の除外には上書きではなく:not()を使う
本質的に無関係な状態や要素を除外したいときは、後から上書きするのではなく:not()で最初から除外します。
■ やるべきこと(Dos)
- 「最後ではない
liにだけ下線を引く」のように、除外したい対象は:not()で最初から外す
.fancy-list li:not(:last-child) {
border-bottom: 1px solid silver;
}
- 並べ替えても結果が変わらないよう
button:hover:not(:disabled)と書く
button:hover:not(:disabled) {
background: var(--color-blue);
}
button:disabled {
background: var(--color-neutral);
}
■ やってはいけないこと(Don'ts)
- 望ましい値を後から打ち消す書き方をしない。別ルールで設定した
border-bottomまで意図せず消すおそれがある
.fancy-list li {
border-bottom: 1px solid silver;
}
.fancy-list li:last-child {
border-bottom: none;
}
-
button:hoverとbutton:disabledを別々に書かない。並べ替えると無効化されたボタンにもhoverの背景色が当たる
button:hover {
background: var(--color-blue);
}
button:disabled {
background: var(--color-neutral);
}
特化のための上書きは問題ない
いっぽうで、対象を特化するための上書きは問題ありません。「ボタンは基本ニュートラル、主要ボタンだけ青」のように、どちらのルールも正当な意図を表しています。
■ やるべきこと(Dos)
- 特化(specialization)のための上書きは使ってよい
button {
background: var(--color-neutral);
}
button.primary {
background: var(--color-blue);
}
深くネストしたサブツリーの除外には:not()より@scopeを使う
:not()と子孫セレクタの組み合わせでもサブツリー(ある要素より下の枝。子孫のまとまり)を除外できます。ただし深くネストした構造ではうまく機能しません。@scopeは階層上の近さを考慮するため、この問題を解決します。
■ やるべきこと(Dos)
- 深くネストしたサブツリーの除外には
@scopeで範囲を区切る
@scope (.card) to (.content) {
/* .card の中にあり、かつ .content の中にはない要素向けのスタイル */
}
これは入れ子のカードでも期待どおりに動作します。
■ やってはいけないこと(Don'ts)
- 深い入れ子の除外を
:not()と子孫セレクタだけで済ませない。たとえば.card :not(.content *)は入れ子のカードで期待どおりに動かない
/* 入れ子のカードでは期待どおりに動かない */
.card :not(.content *) {
/* … */
}
ネスト(nesting)はスコープと使い分ける
ネイティブのCSSネストは、関連するスタイルをまとめて読みやすくする範囲で使います。ただし「詳細度」より「近さ」を優先したいときは、ネストより@scopeが向きます。テーマ用のクラスのように、どんな順でネストされても一番近い対象を勝たせたい場合です。
■ やるべきこと(Dos)
- 可読性と保守性が上がる範囲でネイティブのCSSネストを使う
- 近さを優先したいときはネストより
@scopeを使う
@scope (.dark) {
.invert { color-scheme: light; }
}
@scope (.light) {
.invert { color-scheme: dark; }
}
■ やってはいけないこと(Don'ts)
- 近さが重要な場面でネスト(や子孫セレクタ)に頼らない。次は
.invertが.darkと.lightの両方にネストされると、詳細度が同じため常にダークに解決される
.dark .invert { color-scheme: light; }
.light .invert { color-scheme: dark; }
グローバルリセットは使わない
■ やるべきこと(Dos)
- リセットスタイルは特定の要素種別や条件に対して適用する
■ やってはいけないこと(Don'ts)
-
*に対するスタイル(グローバルリセット)を使わない。Webコンポーネントや優先順位の低いカスケードレイヤーから(!importantなしには)上書きできない
/* グローバルリセットは後から上書きしづらい */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
インタラクティブ性(Interactivity)
フォーカスリングは:focusではなく:focus-visibleで定義する
:focusでフォーカスリングを上書きすると、マウスでクリックしたときにも常にリングが出ます。キーボード操作時だけリングを出したいときは:focus-visibleを使います。
■ やるべきこと(Dos)
- フォーカスリングは
:focus-visibleで定義する - リングには
box-shadowよりoutlineを使い、outline-offsetで要素との間に余白を取る
.button:focus-visible {
/* リングと要素の間に余白を取る */
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
-
box-shadowに頼る場合はforced-colorsメディアクエリでoutlineベースのフォールバックを用意する。box-shadowは強制カラーモードで消えるため
■ やってはいけないこと(Don'ts)
-
:focusでフォーカスリングを定義しない。マウスでクリックしたときにも常にリングが出る -
outline: noneでデフォルトのリングを消したまま、代替の可視フォーカスを用意しない。キーボード利用者を締め出す
/* 代替を用意せず既定のフォーカスを消すとキーボード利用者が困る */
.button:focus {
outline: none;
}
タッチターゲットはmin-block-size/min-inline-sizeで確保する
操作要素は最低24×24 CSSピクセルを確保します(WCAG 2.5.8 Target Size Minimum(AA))。
■ やるべきこと(Dos)
- 大きさは
width/heightではなくmin-block-size/min-inline-sizeやパディングで確保する。中身で大きくはできても、小さくはできなくなる
.icon-button {
/* 中身で広がるのは許容し、24px より小さくはしない */
min-block-size: 24px;
min-inline-size: 24px;
}
- タッチなど粗いポインタ環境では
@media (pointer: coarse)でターゲットを大きくする
■ やってはいけないこと(Don'ts)
- 固定の
width/heightでタッチターゲットを決めない。中身が増えても広がらず、はみ出すおそれがある
/* 中身が増えても広がらず、はみ出すおそれがある */
.icon-button {
width: 24px;
height: 24px;
}
カスタムジェスチャーでtouch-action: noneを使わない
■ やるべきこと(Dos)
- 必要な軸だけに絞る。横スワイプなら
pan-y(縦スクロールは残る)、縦スワイプならpan-x
.carousel {
/* 横スワイプを扱いつつ縦のページスクロールは残す */
touch-action: pan-y;
}
-
noneは描画キャンバスのようにネイティブのタッチ挙動が不要な要素だけに使う
■ やってはいけないこと(Don'ts)
- カスタムジェスチャーのために
touch-action: noneを使わない。その要素を通したページのスクロールができなくなる
/* この要素の上ではページがスクロールできなくなる */
.carousel {
touch-action: none;
}
デザイントークンとテーマ(Design Tokens and Theming)
デザイントークンは:rootのカスタムプロパティで定義する
色・フォント・サイズなどの中核のデザイン変数は:rootのカスタムプロパティとして定義します。デザイン全体で一貫性を保て、チームをまたいでUIをスケールできます。
■ やるべきこと(Dos)
- 中核のデザイン変数は
:rootのカスタムプロパティで定義する - トークンは前の層を踏まえた階層で構成する。スコープが小さいほど層は少なくてよく、簡単なデモなら1層で十分
- 階層1:プリミティブトークン(
--color-blue-10・--color-gray-90・--font-sans-serif・--size-xlなど)。 - 階層2:セマンティックトークン(
--color-accent・--color-neutral・--font-body・--font-headingなど)。 - 階層3:汎用UIのトークン(
--ui-border・--surface-bg-subtleなど)。 - 階層4:コンポーネント固有のトークン(
--button-bg-primary-hoverなど)。
- 階層1:プリミティブトークン(
- 自前の流儀を作る前に既存の命名規則を確認する
■ やってはいけないこと(Don'ts)
- ささいでないスタイリング値をインラインで指定しない。
background: transparentやpadding: 0は問題ないが、background: #f06やpadding: .3emはいけない(例外はテストケースなどコードを小さく保つことが最優先の場面だけ)
/* 値を直接書くと変更時に直し漏れる */
.button {
background: #f06;
padding: .3em;
}
- トークンの階層を過剰に設計しない
ダークモードはcolor-scheme: light darkでシステム設定に追従させる
■ やるべきこと(Dos)
-
:rootにcolor-scheme: light darkを指定してシステム設定に自動追従させる。個別の要素に指定すれば、そのサブツリーだけ別の値を強制できる
:root {
/* システムのライト/ダーク設定に自動追従する */
color-scheme: light dark;
}
light-dark()で配色を自動解決する
light-dark()を使うと、要素のcolor-schemeに応じて第1引数(ライト)か第2引数(ダーク)が選ばれます。通常は階層2か階層3のトークンで使います。
■ やるべきこと(Dos)
- 配色は
light-dark()で自動解決する
:root {
/* color-scheme に応じてライトかダークの値が選ばれる */
--color-surface: light-dark(#ffffff, #1a1a1a);
--color-text: light-dark(#1a1a1a, #f5f5f5);
}
■ やってはいけないこと(Don'ts)
- 継承される
<color>プロパティでlight-dark()を使うとき、子孫のcolor-scheme上書きに追従すると思い込まない。その要素のcolor-schemeで具体的な色に解決され、子孫には解決済みの色が継承される。動的に保ちたいときは、未登録のカスタムプロパティとして値を渡すだけにとどめる
強制カラーモードにフォールバックを用意する
強制カラーモード(Windowsのハイコントラスト)では、ブラウザが作者の色をシステムキーワードで上書きします。background-image・box-shadow・border-imageは取り除かれます。
■ やるべきこと(Dos)
- 色トークンには
@media (forced-colors: active)でシステムカラーのフォールバックを定義する
@media (forced-colors: active) {
.button {
/* システムキーワードへフォールバックする */
border: 1px solid ButtonText;
color: ButtonText;
}
}
- 色が情報そのものを担う場面(シンタックスハイライター、カラーピッカーの見本色など)では
forced-color-adjust: noneを使う
■ やってはいけないこと(Don'ts)
- 境界線・区切り・状態を
background-image・box-shadow・border-imageだけで表現しない。強制カラーモードで消える(印刷でも消えがち)。使うならシステムカラーのキーワード(CanvasText・LinkText・ButtonText・Highlight・GrayTextなど)のoutlineやborderで代替を用意する
/* 強制カラーモードでは影が消え、境界が見えなくなる */
.card {
box-shadow: 0 0 0 1px gray;
}
- 見た目を保ちたいだけの理由で
forced-color-adjust: noneを使わない
濃淡(ティント)は明度チャンネルだけで作らない
色の濃淡を動的に作る前に、まず既存のデザイントークンが使えないか確認します。そのほうがデザイナーの制御が効きます。
■ やるべきこと(Dos)
- 動的に作るなら
color-mix()で白や黒と混ぜる(できればoklab空間で)。色を安全に色域へ収められる。ただし彩度が落ちて色あせがち
:root {
/* oklab 空間で白と 30% 混ぜて明るい濃淡を作る */
--color-primary-tint: color-mix(in oklab, var(--primary), white 30%);
}
- 明度調整を他の手法と組み合わせてもよい。ただし明度調整は30%を超えない
■ やってはいけないこと(Don'ts)
- 明度チャンネルだけを単純に調整しない。理論上は正しい方法だが、ブラウザはまだ色域マッピング(色を表示可能な範囲に収める処理)を実装しておらず、結果の色が予測できない
/* 明度チャンネルだけ調整。結果の色が予測できない */
.tint {
background: oklab(from var(--primary) 0.9 a b);
}
ブラウザ生成UIはまずCSSでテーマを当てる
ブラウザが生成するUIの多くはCSSでカスタマイズできます。モダンな機能でも、古いブラウザで大きく崩れずに表示されます。フォールバックが要らないことも多いです。作り直す前に2点を確認します。
1.モダンなCSSでも必要なだけカスタマイズできないか
2.作り直すトレードオフに見合うほど重要か。作り直すとネイティブUIが無償で提供するアクセシビリティ・キーボード操作・IME・支援技術との連携を失う
■ やるべきこと(Dos)
-
::selectionで選択範囲の色を変える -
accent-colorでチェックボックスなどのブラウザ生成UIにページのアクセントカラーを反映する -
color-schemeでブラウザUIをライト/ダークに追従させる - スクロールバーは
scrollbar-color(つまみとトラックはコントラスト比3:1以上)とscrollbar-widthで調整する - 入力の妥当性は
:user-invalid/:user-validでスタイリングする。ユーザーが操作したあとだけマッチし、読み込み時点で空の必須欄をエラー表示しない -
<input>・<textarea>・<button>は色・ボーダー・背景・タイポグラフィの目的で通常要素として扱える
::selection {
background: var(--color-accent);
color: var(--color-on-accent);
}
.panel {
/* 1つ目がつまみ、2つ目がトラックの色 */
scrollbar-color: var(--color-accent) var(--color-track);
scrollbar-width: thin;
}
input:user-invalid {
border-color: var(--color-error);
}
■ やってはいけないこと(Don'ts)
- 本文テキストに
user-select: noneを使わない。コピー&ペーストや翻訳ツール、支援技術の読み上げを壊す。ドラッグハンドルやツールバーなどのUI部品だけに限る
/* 本文の選択・コピー・読み上げを妨げる */
article {
user-select: none;
}
- スクロール可能な領域に
scrollbar-width: noneを設定しない。noneはスクロールを別の手段で完全に置き換えた場合だけに使う - 妥当性のスタイルに
:invalid/:validを使わない。読み込み直後に、空の必須フィールドをエラー表示してしまう
/* 読み込んだ瞬間、空の必須欄が赤くなる */
input:invalid {
border-color: var(--color-error);
}
テキスト系フィールド(<input>・<textarea>)
色・ボーダー・背景・タイポグラフィの目的では、通常のテキストコンテナとして扱えます。
■ やるべきこと(Dos)
- プレースホルダーは
:placeholder-shownと::placeholderでスタイリングする -
<textarea>はresize: verticalで横方向のリサイズを無効化、resize: noneで全リサイズを無効化する
input::placeholder {
color: var(--color-text-subtle);
}
textarea {
/* 縦方向のリサイズだけ許可する */
resize: vertical;
}
複数選択コントロール(ラジオ・チェックボックス)
■ やるべきこと(Dos)
- ページ内にインラインで並べて選ばせる:各選択肢を
<label>で包んだ<input type=checkbox>か<input type=radio>を使いlabel:has(:checked)でスタイリングする - チェックボックス・ラジオ・スイッチは
appearance: noneと生成コンテンツ(::before/::after)か背景画像でチェック状態を描く
/* チェックされた選択肢のラベルを強調する */
label:has(:checked) {
border-color: var(--color-accent);
}
.custom-checkbox {
/* 既定の見た目を消し、生成コンテンツで描き直す */
appearance: none;
}
テキスト系でない <input>(ボタン・スライダー・ファイル入力など)
■ やるべきこと(Dos)
- ファイル入力:
::file-selector-buttonでボタンをスタイリングする - スライダー:
appearance: noneと、つまみ用(::-webkit-slider-thumb・::-moz-range-thumbなど)・トラック用(::-webkit-slider-runnable-track・::-moz-range-trackなど)の擬似要素で制御する
input[type="file"]::file-selector-button {
background: var(--color-accent);
color: var(--color-on-accent);
}
input[type="range"] {
appearance: none;
}
input[type="range"]::-webkit-slider-thumb {
/* つまみの見た目を独自に描く */
appearance: none;
}
■ やってはいけないこと(Don'ts)
-
typeがbutton・submit・resetの<input>を使わない。<button>を使い通常の要素としてスタイリングする
<!-- 避ける -->
<input type="button" value="送信">
<!-- 代わりに button を使う -->
<button type="submit">送信</button>
原典: Modern Web Guidance / CSS — 5. Design tokens and theming
高コントラスト設定にはprefers-contrastで対応する
アクセントを控えめな色(薄いグレーのスクロールバーなど)で作っている場合は、高コントラストを好むユーザー向けに濃い色の上書きを用意します。すでに十分なコントラストがあるなら不要です。
■ やるべきこと(Dos)
- 低コントラストの装飾を使うときだけ、
@media (prefers-contrast: more)で黒×白のようなはっきりした色を上書きする
@media (prefers-contrast: more) {
.scroller {
--scrollbar-thumb: #000000;
--scrollbar-track: #ffffff;
}
}
フォーム部品をCSSでテーマする(Form controls)
フォーム部品の見た目もCSSだけで整えられます。formsガイドから、JavaScriptに頼らずCSSで完結する指針を補足します。
自動入力された欄は:autofillでハイライトする
:autofillは、ブラウザが自動入力し、ユーザーがまだ編集していないフィールドにマッチするCSS疑似クラスです。<input>・<select>・<textarea>で使えます。入力済みの欄を目立たせ、フォーム完了まで迷わせないために使います。なお自動入力された欄ではbackground-colorを直接上書きできません。背景はbox-shadowのインセットで作ります。
■ やるべきこと(Dos)
-
:autofillで自動入力済みの欄に独自の枠線や背景を当てる。背景はbox-shadowのインセットで作る - 状態は色だけに頼らず、枠線の太さや背景の濃淡など複数の手がかりで示す
- 自動入力欄をスタイルするときも、
:focus-visibleで明示的な高コントラストのフォーカス表示を必ず用意する
input:autofill,
input:-webkit-autofill {
/* 色だけに頼らず、枠線と背景の両方で状態を示す */
border: 2px solid #2e7d32;
box-shadow: 0 0 0 100vmax #e8f5e9 inset;
}
/* 必須: 自動入力欄をスタイルするときも明示的なフォーカス表示を用意する */
input:autofill:focus-visible,
input:-webkit-autofill:focus-visible {
outline: 3px solid #000;
outline-offset: 2px;
}
■ やってはいけないこと(Don'ts)
- 自動入力済みの状態を枠線の色だけで示さない。色覚特性によっては気づけない
- フォーカスの輪郭(
outline: none)を、代替の高コントラスト表示なしに消さない - 疑似クラス名を
:auto-fillと書かない。正しくは:autofill
:autofillはプログレッシブエンハンスメントです。非対応のブラウザ(Firefoxなど)でもフォームは普通に機能し、ハイライトが付かないだけです。JavaScriptのフォールバックは不要です。
レスポンシブデザイン(Responsive design)
ビューポートではなくコンテナの幅に合わせるなら@containerを使う
@containerクエリは、要素を親コンテナの大きさに応じて切り替える仕組みです。画面幅を基準にするメディアクエリと違い、置かれた場所のコンテナ幅でレイアウトが変わります。
■ やるべきこと(Dos)
- 要素を親コンテナの大きさに応じて切り替えるなら
@containerを使う。同じコンポーネントをサイドバーにもメインにも置ける。コンテナ幅を基準にしたサイズ単位cqiなどとあわせて使う
なおdvh / dvwといったビューポート単位や100vwの扱いは別のセクションで扱います。
メディア要素にはaspect-ratioを指定する
■ やるべきこと(Dos)
-
<img>や<video>にはaspect-ratioで縦横比を指定し、読み込み中に表示領域を確保する。要素が後から表示されてレイアウトがずれるCumulative Layout Shift(CLS)を防げる
レスポンシブなフォントサイズにはclamp()を使う
ビューポート相対の単位とフォント相対の単位をclamp()の中で組み合わせます。画面幅に応じてフォントサイズが伸縮しつつ、上限と下限の範囲に収まります。
■ やるべきこと(Dos)
- ビューポート相対とフォント相対の単位を
clamp()で組み合わせる。割合を変えると、フォントサイズが変化する速さを調整できる
.title {
font-size: clamp(2rem, 1rem + 5vw, 4rem); /* 下限 2rem、上限 4rem の範囲で可変 */
}
■ やってはいけないこと(Don'ts)
-
vwだけでfont-sizeを指定しない。clamp()がないと、極端に大きい画面や小さい画面で文字が大きくなりすぎたり小さくなりすぎたりする
/* 極端な画面幅で文字が破綻する */
.title {
font-size: 5vw;
}
タイポグラフィ(Typography)
line-heightには単位なしの数値を使う
■ やるべきこと(Dos)
-
line-heightには1.5のように単位なしの数値を指定する。フォントサイズを継承するとき、その倍率で行の高さも一緒に拡大縮小される
長いURLにはoverflow-wrap: break-wordを使う
■ やるべきこと(Dos)
- 長いURLのはみ出しは
overflow-wrap: break-wordで防ぐ。どうしても収まらないときはanywhereも使える
font-sizeにpxを使わない
■ やるべきこと(Dos)
-
font-sizeにはremを使い、ユーザーがブラウザで設定したフォントサイズ(ルートのフォントサイズ)を尊重する。文脈に応じたサイズにはemを使う
■ やってはいけないこと(Don'ts)
-
font-sizeにpxを使わない。ユーザーのフォントサイズ設定を無視してしまう
/* ユーザーのフォントサイズ設定を無視する */
body {
font-size: 16px;
}
見出しにはtext-wrap: balance、本文にはtext-wrap: prettyを使う
text-wrapは行の折り返し方を制御するプロパティです。
■ やるべきこと(Dos)
- 見出しや見出しに準ずる要素(
<th>など)にはtext-wrap: balance。各行の長さが揃う - 段落や引用などの長い本文には
text-wrap: pretty。「1単語だけ次の行」を防ぐ
.heading { text-wrap: balance; } /* 見出し向け。各行の長さを均す */
.prose { text-wrap: pretty; } /* 本文向け。孤立行を防ぐ */
■ やってはいけないこと(Don'ts)
-
balanceやprettyを*(全要素)に当てない。パフォーマンスのコストがある
/* 全要素に当てるとパフォーマンスのコストが大きい */
* {
text-wrap: pretty;
}
- 背景・枠線・影などで囲まれた要素では
text-wrap: balanceを避ける。balanceはコンテナの幅を変えず、その幅の中での折り返し方だけを変えるため、右端に余白が残りやすい
折り返しを止めるならtext-wrap: nowrapを使う
ナビゲーションタブや横スクロールのチップなど、折り返すと崩れる要素では折り返しを止めます。white-space: nowrapより意味の伝わるtext-wrap: nowrapを使います。
■ やるべきこと(Dos)
- 折り返しを止めるには
text-wrap: nowrapを使う - はみ出しは
overflowで処理する。省略記号text-overflow: ellipsisを出すならoverflow: hiddenを併せる
.no-wrap {
text-wrap: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
Webフォントのズレはfont-size-adjustで抑える
Webフォントが読み込まれると、同じfont-sizeでも代替フォントと文字の大きさが変わり、レイアウトシフト(CLS)や読みにくさが起きます。font-size-adjustは、フォントのx-heightなどを基準に大きさをそろえ、どのフォントでも見た目の大きさを保ちます。
■ やるべきこと(Dos)
-
font-size-adjust: from-fontで主フォントのx-heightを基準に代替フォントを正規化する。読み込み中や失敗時のCLSを防ぐ - 大文字主体の見出しは
cap-height from-font、等幅はch-widthなど基準を選べる
.text-content {
font-family: "MyWebFont", "Arial", sans-serif;
/* 主フォントの x-height に代替フォントを合わせ、CLSを防ぐ */
font-size-adjust: from-font;
}
視覚効果(Visual effects)
影を重ねて奥行きを表現する
■ やるべきこと(Dos)
- 複数の影を重ねて奥行きを出す
- 非矩形の図形や透過PNGには
box-shadowではなくfilter: drop-shadow()を使う。要素の不透明な形に沿って影が付く
光のオーバーレイにはmix-blend-mode/background-blend-modeを使う
ブレンドモードは、重なった色をどう混ぜて描画するかを指定する機能です。
■ やるべきこと(Dos)
- 光が当たったようなオーバーレイには
mix-blend-modeとbackground-blend-modeを使う。効果の及ぶ範囲はisolation: isolateで限定する
.hero {
background-image: url('texture.png'), linear-gradient(to bottom, #fff, #eee);
background-blend-mode: soft-light; /* テクスチャと背景を柔らかい光として合成 */
}
比率の取れたカーブには楕円形のborder-radiusを使う
■ やるべきこと(Dos)
- 要素を増やさず比率の取れたカーブには楕円形の
border-radius(例10px / 20px)を使う
グラデーションの補間にはoklchかoklabを指定する
oklchやoklabは、色を人の見た目に近い形で扱う新しい色空間です。
■ やるべきこと(Dos)
- グラデーションや
color-mix()では、補間する色空間をin oklchかin oklabで明示する-
in oklch:彩度(chroma)をよく保つ。ただし色の差が大きいとデバイスの色域からはみ出しやすい -
in oklab:色域に収まりやすい。ただし中間でくすんだ色になりやすい(特に正反対の色相を補間するとき)
-
■ やってはいけないこと(Don'ts)
- 特別な理由がない限り
in srgbを使わない。srgbで補間する必要があるカラーピッカーを作る場合などが例外
古いブラウザにはフォールバックを用意する
2024年より前の一部ブラウザは、グラデーションの補間色空間に対応していません。
■ やるべきこと(Dos)
- 変数を定義し、安全なときだけトークンを使う
:root {
--in-oklab: ;
--in-oklch: ;
}
@supports (linear-gradient(in oklab, white, black)) {
:root {
--in-oklab: in oklab;
--in-oklch: in oklch;
}
}
使うときは次のようにします。
.card {
background: linear-gradient(to bottom var(--in-oklab), var(--accent-color), var(--darker));
}
■ やってはいけないこと(Don'ts)
- この手法を使うとき、変数の前に置く固定の記述(例:
to bottom)を省かない。これがないと、古いブラウザで変数が空になったときに構文エラーになる
なおcolor-mix()にはこの手法は不要です。color-mix()に対応するブラウザは、そのin <色空間>引数にも対応しています。
模様はCSSグラデーションとハードストップで描く
多くの模様は、CSSグラデーションとハードストップ(色を急に切り替える指定)で作れます。SVGや外部画像より軽いことがあります。周囲の文脈からCSS変数や長さを参照できるためです。位置を2回繰り返す必要はありません。0または0%だけ書けば、グラデーションの補正で自動調整されます。
■ やるべきこと(Dos)
- 模様はCSSグラデーションとハードストップで描く
▼ それぞれ幅1emの縦ストライプ
.box {
background: linear-gradient(to right, var(--color-1) 50%, var(--color-2) 0) 0 / 2em;
}
▼ それぞれ幅1emの斜めストライプ
.box {
background: repeating-linear-gradient(-45deg, var(--color-1) 0 1em, var(--color-2) 0 2em);
}
▼ 1emの正方形によるチェッカーボード
.box {
background: repeating-conic-gradient(var(--color-1) 0 25%, var(--color-2) 0 50%) 0 / 2em 2em;
}
▼ 半径.5emの点を2em間隔で並べた水玉
.box {
--distance: 2em;
--radius: .5em;
--polka: radial-gradient(circle, var(--color-1) var(--radius), transparent calc(var(--radius) + 1px));
background: var(--polka) 0 0, var(--polka) var(--distance) var(--distance) var(--color-2);
background-size: calc(var(--distance) * 2) calc(var(--distance) * 2);
}
▼ 円グラフ
.pie {
--p: 80%;
width: 60px;
aspect-ratio: 1;
border-radius: 50%;
background: conic-gradient(var(--color-1) var(--p), transparent 0%) var(--color-2);
}
■ やってはいけないこと(Don'ts)
- グラデーションでグラフを描くとき、テキストの代替なしで済ませない。スクリーンリーダー向けに意味のあるデータテーブルを必ず併設する。詳しくはMWGアクセシビリティガイドの代替テキスト・メディアの指針を参照してください
アニメーションとトランジション(Transitions & animations)
凝った演出にはclip-path・mask-image・ビュートランジションを使う
■ やるべきこと(Dos)
- 図形を切り抜きながら見せる演出には
clip-pathとmask-image。mask-imageは別の画像を使って要素の見える範囲を切り抜く機能で、滑らかなフェードアウトも作れる - 複雑なレイアウト状態の切り替えには、ビュートランジション(View Transitions)
アニメーションはopacityとtransformを優先する
opacityとtransformは、レイアウト計算とは別のスレッド(コンポジタ)で処理されます。レイアウトを再計算せずにアニメーションを動かせます。
■ やるべきこと(Dos)
-
opacityとtransform(translateなどの個別プロパティを含む)を優先する
.panel {
transition: translate 0.2s; /* 別スレッド(コンポジタ)で処理される */
}
■ やってはいけないこと(Don'ts)
-
leftやtopのようにレイアウトを伴うプロパティをアニメーションしない。描画コストが高くカクつきの原因になる
.panel {
transition: left 0.2s; /* レイアウトが走る */
}
displayの切り替えはallow-discreteと@starting-styleでアニメーションする
displayや<dialog>の開閉のような状態は、本来アニメーションできません。
■ やるべきこと(Dos)
-
transition-behavior: allow-discreteと@starting-styleでネイティブにアニメーションさせる。@starting-styleは、要素が表示され始める瞬間の初期スタイルを指定する仕組み
.popover-reveal {
/* display の切り替えにアニメーションを許可する */
transition: display 0.2s allow-discrete;
}
content-visibilityには必ずcontain-intrinsic-sizeを添える
content-visibility: autoは、画面外の要素の描画を後回しにして表示を速くする機能です。
■ やるべきこと(Dos)
-
content-visibilityには必ずcontain-intrinsic-sizeを組み合わせる。描画をスキップした要素に仮の寸法を与えるため -
contain-intrinsic-sizeにはautoキーワードと、中身から見積もった値を指定する。単位はpxではなくrem・lh・cap・chを優先する。アイテムごとに大きさがばらつくなら平均値を使う
.large-section {
content-visibility: auto;
contain-intrinsic-block-size: auto 800px;
}
.row {
--row-gap: 0.4rem;
--title-height: 1lh;
--description-height: 0.85lh;
display: grid;
row-gap: var(--row-gap);
content-visibility: auto;
/* タイトル・行間・説明文の高さの合計を、描画スキップ時の寸法にする */
contain-intrinsic-block-size: auto calc(
var(--title-height) + var(--row-gap) + var(--description-height)
);
}
■ やってはいけないこと(Don'ts)
-
content-visibilityをcontain-intrinsic-sizeなしで単体で使わない。スクロールバーが飛び跳ね、レイアウトシフト(CLS)の原因になる
containでコンポーネントの再描画を閉じ込める
■ やるべきこと(Dos)
- コンポーネント単位で描画更新を切り離すには
contain: layout style paintを使う。containは、要素の内部の変更が外側に波及しないとブラウザに伝える機能で、再描画の範囲を狭められる
prefers-reduced-motionで激しい動きを抑える
■ やるべきこと(Dos)
-
prefers-reduced-motionメディアクエリで激しいモーションをオフにする。減らし方は個別に対応するか、カスタムプロパティを使う
@property --animation-reduced {
syntax: "*";
inherits: false;
initial-value: none;
}
@media (prefers-reduced-motion: reduce) {
* {
animation: var(--animation-reduced) !important;
}
}
この方法なら、減らした版のアニメーションを元のアニメーションと同じ場所にまとめて書けます。
progress:not([value]) {
animation: slide 1s infinite linear;
--animation-reduced: slide 20s infinite linear;
}
■ やってはいけないこと(Don'ts)
- 全要素に
animation-duration: 0.01msを一律で当てない。一部のアニメーションがかえって不快になることがある
/* 一律に止めると、かえって不快になるアニメがある */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
}
}
バネ・バウンドにはlinear()イージングを使う
linear()は、複数の停止点で複雑なカーブを近似するイージング関数です。ease-inやcubic-bezier()では作れないバネやバウンドを表現できます。停止点はツールで生成します。
■ やるべきこと(Dos)
- バネ・バウンドのような物理的な動きは
linear()の多段ストップで表現する。停止点はカスタムプロパティに入れて使い回す -
linear()は時間を計算しないのでdurationを必ず指定する - なめらかさとCSSサイズはトレードオフ。停止点を増やしすぎない
.spring {
--spring-easing: linear(0, 0.06 1%, 1.116 5.4%, 0.937 14.3% /* …停止点は省略… */, 1);
/* 必須: linear() は duration を自動計算しない。必ず指定する */
transition: scale 0.8s var(--spring-easing);
}
■ やってはいけないこと(Don'ts)
- バウンドのイージングを
opacityに当てない。値が0未満や1超に行き過ぎてチラつく
単一の変形だけ動かすならtranslate・rotate・scaleを使う
transformは複数の変形をまとめて指定するため、1つだけ変えるにも全体を書き直す必要があります。個別プロパティ(translate・rotate・scale)なら、:hoverなどで1つの変形だけを上書きできます。適用順は記述順によらずtranslate→rotate→scale→transformの固定です。
■ やるべきこと(Dos)
- 状態変化で1つの変形だけ変えるなら個別プロパティを使う。重なり合うアニメーションを衝突させずに定義できる
- 状態変化(
:hoverなど)で変形する要素には、基底に変形なしの初期値(translate: 0・scale: 1など)を置く。変形時にスタッキングコンテキストが急に変わるのを防ぐ
.card {
animation: float 3s infinite ease-in-out; /* translate を動かす */
transition: scale 0.3s ease;
scale: 1; /* 変形なしの初期値。突然のスタッキングコンテキスト変化を防ぐ */
}
.card:hover {
scale: 1.05;
}
■ やってはいけないこと(Don'ts)
- 「先にscale、後でrotate」のように別の順序が必要なときは個別プロパティを使わない。順序は固定なので
transform関数で書く
生成コンテンツ(Generated content)
contentで意味のあるテキストを表現しない
■ やってはいけないこと(Don'ts)
- ラベルや状態、操作の説明など、意味を持つテキストを
contentで出さない。これらはDOM側に置く(WCAG F87)
/* 意味のあるテキストを CSS に置かない */
.badge::before {
content: "新着";
}
contentの代替テキスト引数は、装飾のつもりが意味を持ってしまった場合の被害を減らす手段です。意味のあるテキストをCSSに置く言い訳には使えません。
contentの代替テキスト引数を正しく使う
contentのスラッシュ以降に書くテキストは、スクリーンリーダー向けの代替テキストになります。
■ やるべきこと(Dos)
- アイコン画像に読み上げ用の文言を添える
.icon-save::before {
content: url(cloud.svg) / "Save"; /* スクリーンリーダーは「Save」と読む */
}
- 純粋に装飾のテキストを読み上げさせたくないときは、代替テキストを空にする
.decorative::before {
content: "•" / ""; /* 装飾なので読み上げさせない */
}
- 代替テキスト引数は、その値が主たる値と異なり、かつDOMにまだ無いときだけ使う
■ やってはいけないこと(Don'ts)
- 画像に空の代替テキスト引数を付けない。画像はもともと装飾扱い
.icon::before {
content: url(cloud.svg) / ""; /* 画像はもともと装飾扱いなので不要 */
}
- 絵文字の説明は、公式の絵文字名と同じなら付けない(公式名と違う意図のときだけ付ける)
.party::before {
content: "🎉" / "celebration"; /* 公式名とほぼ同じなので不要 */
}
正しくは、公式名と違う意図を伝えるときだけ付けます。
.party::before {
content: "🎉" / "Yay!"; /* 公式名と違うので意味がある */
}
- DOMにある値と同じ代替テキストを付けない。二重に読み上げられる
<button class="save">Save</button>
button.save::before {
content: url(cloud.svg) / "Save";
}
このとき、スクリーンリーダーは「Save save」と読み上げてしまいます。
さいごに
ここまでCSSの2つのガイドからDos / Don'tsを網羅しました。量は多いですが、ひとつひとつは「古い手癖を新しい標準に置き換える」だけのものです。
筆者はCSSの新機能を追うのが好きです。それでも全部を覚えてはいられません。:has()や@scope、light-dark()、text-wrap: balanceのように「知っていれば一行で済む」機能は、思い出せないと結局JavaScriptや回りくどいCSSに逃げてしまいます。このガイドはその抜けを埋めてくれます。
気になった項目から、自分のCSSを見直してみてください。AIエージェントに書かせるなら、冒頭で紹介したスキルをそのまま入れるのが手っ取り早いです。
出典・ライセンス
本記事は Modern Web Guidance(Google Chrome、Apache License 2.0)を日本語へ翻訳・再構成し、筆者の見解や補足を加えたものです。コード例への日本語コメント追加など、原文に変更を加えています(逐語訳ではありません)。参照したガイドは各セクション末尾にリンクしています。「Google」「Chrome」は各社の商標で、出典明示のために使用しています。
Discussion