コンテンツにスキップ

CSS

CSSは、時流、制作物、チーム編成によって設計や制作手法が流動的に変化するものです。
ここではいくつかの原則とモダンCSSのベストプラクティスに基づいた、破綻しにくくスケーラブルなCSSの作り方について説明します。

はじめに

以下はやめてほしいリストです。

  • 自分の諸問題を解決するために詳細度を高くする
  • プロジェクトの一貫性に従わない
  • 無意味な記述
  • デザインを再現したら終わり、記述内容を精査しない
  • スタイルについてのことをhtmlの修正で対応しようとする
  • 他人のコードを理解せずペーストする

Stylelintに適合させる

Stylelintを用いることで、プロパティの並び順や記述ルールの統一を促すことができます。 CSS(またはスタイル)を記述する場合は必ず導入してください。

Stylelint設定ファイル:monosus/lint-tools/.stylelintrc.json

Stylelintのルールにどうしても適合できない箇所については行単位でignoreコメントを設定します。

/* 特別な理由で!importantを使用するケース。。。 */
#id {
/* stylelint-disable-next-line declaration-no-important */
color: pink !important;
}

フォーマッター

フォーマッターについては適用時の速度の面からBiomeを推奨します。もちろんPrettierでも構いません。

Biome設定ファイル:monosus/lint-tools/biome.json

カスケードレイヤー(@layer)の使用

参考リソース:@layer - CSS: カスケーディングスタイルシート | MDN

@layerを使用することで詳細度と記述順をレイヤーごとで管理出来るようになります。CSSをマルチファイル化することでレイヤーに沿ったファイル構成が可能となり、スタイル管理がたやすくなります。

スタイルの詳細度は@layerでコントロールします。(!importantを絶対に使用しない

HTMLの章で説明した通りレイヤー順を宣言し、@layerを使用しないスタイルは記述しません。

src/assets/css/common/tokens.css
@layer tokens {
:root {
--system-base-color:#fff;
/* ... */
}
}

推奨レイヤー

テンプレートとして推奨するレイヤーは以下です。

レイヤー名説明
resetリセットCSSを個別管理します
tokensrootなどで宣言するグローバル変数
basehtmlやbody、font-familyやベーシックなリンクスタイル、imgへのheight:autoなどサイト共通の基本設定
layout主にランドマークのスタイリング(ヘッダー、フッター、パンくずなど)
componentsページ内で複数回使用するUIコンポーネント
contentsコンテンツ内での要素間の余白など
pageそのページ固有のスタイル
operational運用デリバリーを早めるために一時的にスタイルを上書きするためのレイヤー

上記をベースとしてプロジェクトの特性に合わせて増減させてください。

CSSのファイル構成

各CSSの役割によってファイル内に記述するルールは変化します。上記の推奨レイヤーに沿って説明します。

reset、tokens、base、contents

レイヤーごとの一つのファイルに網羅的に記述します。ファイル名は

  • reset.css
  • tokens.css
  • base.css
  • contents.css

としてsrc/assets/css/common/配下に作成します。後からジョインしたメンバー(または2週間後のあなた)のために適切にコメントを記述します。

src/assets/css/common/contents.css
@layer contents {
/* セクション内の基本の上余白 h2下はデザインルール参照 */
section > :not(h2) + p {
margin-block-start: var(--system-between-element-md);
}
/* h2下の余白ルール */
h2 + * {
margin-block-start: var(--heading-lv2-bottom-margin);
}
}

page、operational

ページごとを対象とするためファイル名を、about.cssやabout-operational.cssなどとして目的だけに閉じたファイル名にし、src/assets/css/{ページ名}/配下に作成します。

運用時にサイト全体を修正したい場合はsrc/assets/css/common/operational.cssとして構いません。

layout、components

各コンポーネント・ランドマーク毎にsrc/assets/css/_import/配下にファイルを作成し、 src/assets/css/common/{components,layout}.cssにそれぞれpostcssなどでインポートします。
一つのコンポーネント(またはランドマーク)ファイルは一つのコンポーネント(またはランドマーク)についてのみ記述します。

src/assets/css/common/components.css
@import "../_import/card-group.css";
/* さらにその他のファイルもインポートします */
src/assets/css/_import/card-group.css
/* 👍Good */
@layer components {
.card-group {
/* card-group のスタイル */
}
}
/* ⛔Bad */
@layer components {
.card-group {
/* card-group のスタイル */
}
.media-list {
/* card-groupとは関係のないスタイル */
}
}

ネスト

スコープするセレクターを起点にCSSネストを使用してください。 ネストの深さは@layerを除いて1階層にとどめます。ただし、擬似クラスやメディアクエリは対象セレクターの中で記述します。 @layerの直下のブラケットは1ペアだけになります。

⛔非推奨

src/assets/css/_import/card-group.css
@layer components {
.card-group {
--card-group-columns: 3;
--card-round: 14px;
display:grid;
grid-template-columns: repeat(var(--card-group-columns), minmax(0,1fr));
gap: var(--spacing-between-element-xs);
> article {
border:var(--system-border-base);
border-radius: var(--card-round);
/* ⛔ネストが深い */
header {
font-size: var(--system-text-xl);
font-weight: bold;
}
}
}
/* ⛔メディアクエリがセレクターを内包している */
@media (width < 48.75rem) {
.card-group {
--card-group-columns: 1;
}
}
}

👍推奨

src/assets/css/_import/card-group.css
@layer components {
.card-group {
--card-group-columns: 3;
--card-round: 14px;
@media (width < 48.75rem) {
--card-group-columns: 1;
}
display:grid;
grid-template-columns: repeat(var(--card-group-columns), minmax(0,1fr));
gap: var(--spacing-between-element-xs);
> article {
border:var(--system-border-base);
border-radius: var(--card-round);
}
> article header {
font-size: var(--system-text-xl);
font-weight: bold;
@media (width < 48.75rem) { /* 擬似クラスやメディアクエリは2階層目以降を使用して構わない */
text-decoration: underline;
}
}
}
}

大きなアニメーションはOSの設定で除外できるようにする

@media prefers-reduced-motion: reduce内で視差効果のない(または少ない)アニメーションを指定します。

プロジェクトとして問題がなければbaseレイヤーでまとめて視差効果抑制の対応をしてしまうことを推奨します。

src/assets/css/common/base.css
/* same base style ... */
/* OSレベルで視差効果を抑制していることを検知してアニメーションを除去する */
@media (prefers-reduced-motion: reduce) {
html:focus-within {
scroll-behavior: auto;
}
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* ... same base style */

デバイス幅のためのメディアクエリ

メディアクエリはコードの可読性を下げることがよくあるため、可能な限りメディアクエリを少なく記述出来るようにします。

デバイスによって非表示にしたい場合

クラスによる出し分けはしません。以下のようにtokens(またはbaseでも構いません)レイヤーでCSS変数としてdisplayプロパティを設定します。

:root {
--sp-hide: ;
--pc-hide: none;
@media (width < 900px) {
--sp-hide: none;
--pc-hide: ;
}
}
/* 使用例:スマートフォン幅で非表示にする */
.some-component {
display: var(--sp-hide);
}

デバイスによってプロパティが追加される場合

デバイスによってプロパティが削除されるような記述は控えてください。

プロパティが追加される場合はメディアクエリを対象セレクタ内で使用します。

/* ⛔Bad セレクター内でメディアクエリを使用していない。プロパティを打ち消している */
.some-component {
padding: 10px;
border: 2px solid blue;
}
@media (max-width: 900px) {
.some-component {
border: none; /* プロパティを打ち消している */
}
}
/* 👍 Good */
.some-component {
padding: 10px;
@media (min-width: 899px) {
border: 2px solid blue;
}
}

デバイスによって値のみが変わる

例えばテキストサイズをデバイスによって変化させる場合。 以下の3つの段階の対応方法を検討してください。

step1:tokensですべて対応する。

デメリット:イレギュラーに対応しにくい、ルールを増やすと混乱する
メリット:低レベルレイヤーでデザインシステム的にスタイルを管理できる

step2:root内でCSS変数を用いた画面幅のトグル操作を行う

デメリット:ある程度の理解力を必要とする
メリット:ブラケットの数がない分の可読性を確保できる。メディアクエリを低レベルレイヤーで一括管理できる

step3:コンポーネント内で設定する

デメリット:同じ記述が複数ファイルで発生すると管理コストが上がる
メリット:レガシーコードに近いため読み慣れている

/* step1の例 */
:root {
--text-size-lg:1.25rem;
@media (width < 900px) {
--text-size-lg:1.125rem;
}
}
.some-component {
font-size: var(--text-size-lg);
}
/* step2の例 */
:root {
--mq-sm: initial;
--mq-pc: ;
@media (width < 900px) {
--mq-sm: ;
--mq-pc: initial;
}
}
/* tokenを無視してPCではfluid、スマートフォンでは固定サイズという特殊な対応 */
.some-component {
--text-size-when-sm: var(--mq-sm) 2rem;
--text-size-when-pc: var(--mq-pc) clamp(1.5rem, -3.804rem + 9.43vw, 5.625rem);
font-size: var(--text-size-when-pc, var(--text-size-when-sm));
}
/* step3の例 */
.some-component {
--some-component-text-size: var(--text-size-lg);
@media (width > 900px) {
--some-component-text-size: 1.5rem;
}
font-size: var(--text-size-lg);
}

更に発展した例:
#font-sizeはclamp()関数で指定する | 当ブログのレスポンシブコーディングについて| TAKLOG

マウスオーバーのスタイリングに画面幅を使用しない

media any-hoverを使用します。

@media (any-hover: hover) {
.link:hover,.link[data-state="hover"] {
background: rgb(0 0 0/ 0.7);
}
}

セレクター

現在は@layer:where()を使用できるためセレクターによる詳細度の違いを気にする必要性は低減しています。
基本的にはクラス属性をセレクターとして使用しますが、BEMなどのレガシーなCSS設計は可能な限り避けます。 HTMLのすべての要素にクラス属性を付与することは時間がかかるだけでなく管理コストも増加します。
スコープクラス(コンポーネントのルートとなるクラス)から直接参照可能なセレクターにクラスは必要ありません。

/* ⛔ not cool */
.card {
.card__body {}
.card__header{}
}
.card-header {
.card-header__title {}
}
/* 🙏 Try this! */
.card-group {
> article { /* .card */}
> article > header {/* .card__header same thing */}
> article > div { /* .card__body same thing */}
> article > footer { /* .card__footer same thing */ }
> h3 { /* !? .card-header__title same thing */ }
}

:where擬似クラスを使用する場合はスコープを絞る

:where擬似クラスを使用する場合、詳細度が上がらない代わりに上書きしてももとの記述の効果が続いてしまいます。
例えば上下に開閉するUIでdivの中にdivを置かなければならないケースでは、直下セレクター>などを使用して有効範囲を具体的にします。

/* ⛔Bad */
.some-component {
:where(div) {
/* ... */
}
}
/* 👍Good */
.some-component {
:where(> div) {
/* ... */
}
}

ステートクラス(アクティブクラス)を使わない

.is-open.actionのようなJavaScriptからクラスを操作するのは避けてください。 代わりに

  • [open]
  • [:popover-open]
  • [aria-expanded=“true”]
  • [:checked]

などの状態を司る属性を使用します。

/* ⛔ Bad */
.accordion.is-open {
/* ... */
}
/* 👍 Good */
details[open] {
/* ... */
}
/* or */
.accordion > button[aria-expanded="true"] + div {
/* ... */
}

その他に、例えばスクロール位置によってヘッダーを追従させる場合はCSS変数をJavaScriptで切り替えることで対応します。

/* stickyが適さないフローティングヘッダー */
header {
--header-state:relative;
/* ... */
}
document.addEventListener('scroll', () => {
const header = document.querySelector('header');
if (window.scrollY > 0) {
header.style.setProperty('--header-state', 'fixed');
} else {
header.style.setProperty('--header-state', 'relative');
}
});

画像よりもCSSで描画できないか検討する。

リソースのリクエスト回数や、ダウンロードサイズを考えた場合にCSSで表現出来るものはCSSで実現してください。

代表的な例としてシェブロン、演算子、三角形などが考えられます。
陰影などで凝った表現をしている場合はベクター画像を使用してください。

考え方の優先順位

以下は一般的なケースにおいての優先度です。リソースのサイズやウェブサイトの状況によって変わってきます。(Webフォントをすでに使用していてキャッシュされる可能性が高いなど)

  1. CSSのみ
  2. SVGファイル
  3. 画像(webp、avif、pngなど)
  4. Webフォントを使用する

理論プロパティを積極的に使用する

margin-inlineinsetなど論理的で明瞭な指定方法を積極的に使用してください。

参考リソース

ショートハンドを控える

ショートハンドを使用するのは

  • borderoutline
  • 4辺に明確に余白を指定するためのpaddingmargin

くらいでしょうか。無理にとは言いませんが、クリーンなコードのためにショートハンドは控え、冗長に感じても個別にプロパティを宣言することを優先してください。

CSS では、必要なことだけを行い、それ以上のことは行わないことが重要です。

CSS Shorthand Syntax Considered an Anti-Pattern – Harry Roberts – Web Performance Consultant

参考リソース:

単位

このセクションはアクセシビリティの観点から非常に重要です。

  • フォントサイズにはrem、emを優先して使用します
  • 縦の余白にはremを優先して使用します
  • 装飾のためのborderにはpxを使用します
  • ボタンUIの横幅(inline-sizeまたはwidth)はrem、emを優先して使用します
  • 横軸の余白はpxを優先して使用します。相対的にしたい場合は%dvwなどを使用します

上記はあくまで参考です。重要なことはルートのフォントサイズを拡大した際にコンテンツの視認性が失われないかを確認することです。実装中はできる限り拡大時の見え方を確認してください。(cmd + ではなくルートのフォントサイズを拡大します。) デザインの美しさを拡大時にも保つためすべてをremとしてしまうのは早計です。あるユーザーは情報を得るために読めるサイズまで文字を大きくしています。1行に表示される単語数が余白によって減ってしまうことは読みやすさに繋がらないため、状況によって使用する単位を判断する必要があります。

html のフォントサイズを62.5%に絶対にしない

絶対にしないでください。あなたが関わるプロジェクトで、修正対象ではない既存リソースにこの記述がある場合、可能な限り修正すべき事象であることをサイトオーナーへ伝えてください。(見積もりが変わってしまうことも含めて)
コーディングを簡単にするためにコーディングしているわけではありません。ルートのフォントサイズはユーザーエージェントに従います。

色を単語で指定しない

/* ⛔ Bad */
p {
color: black;
}

HEX、RGB、HSLなど色を指定する要素に適したものを使用します。
複雑な管理をしない場合はHEX(16進数文字列表記)値で問題ありません。

CSS変数を使用する

以下の値は:rootでグローバル変数として管理します。

  • フォントサイズ
  • 行間
  • 縦余白
  • 角丸
  • ボーダー
  • UI固有の横幅(場合によっては縦幅)
  • トランジションスピード
  • 背景画像を用いたアイコンの背景画像パス
  • z-index

その他にも複数回登場する画角のバリエーションなど変数で管理すべきものは:rootに変数として格納します。
デザインデータから抽出可能なものはコンポーネント作成やページ量産より前に可能な限り収集しておきます。

時差アニメーションを使用する場合はtransition-propertyを指定する

/* ⛔ Bad transitionの対象プロパティ全てに時差設定がされてしまう */
.link {
/* ... */
transition: var(--speed-md); /* 0.4s */
}
/* 👍 Good */
.link {
/* ... */
transition: var(--speed-md);
transition-property: background-color;
}

!importantを絶対に使用しない

念の為ガイドラインに掲載します。いつでも!importantは不要です。

スタイルの詳細度は@layerで管理します。(カスケードレイヤー(@layer)の使用