マジックナンバーを排除する CSS の書き方

CSS の中に top: -3pxwidth: 347px のような数値が唐突に現れることがあります。この数値がなぜその値なのか、コードからは読み取れません。こうした根拠の不明な数値をマジックナンバーと呼びます。

マジックナンバーの問題

以下のコードを見ただけでは、なぜ 37px なのか分かりません。

.dropdown {
  top: 37px;
}

この 37px はおそらくヘッダーの高さに合わせた値ですが、ヘッダーの高さが変わっても 37px はそのまま残り続けます。修正するときにはヘッダーとドロップダウンの関係を知っている人が、両方のファイルを突き合わせて確認しなければなりません。

マジックナンバーが引き起こす問題は大きく 2 つあります。

変更に弱い

値の根拠が別の場所にあるため、元の値が変わったときに追従できない。修正漏れが起きやすい。

意図が伝わらない

数値だけでは「何に合わせた値なのか」「なぜこの数値なのか」が分からない。レビューでも見落とされやすい。

カスタムプロパティで意味を与える

CSS カスタムプロパティを使えば、数値に名前を付けて根拠を明示できます。

:root {
  --header-height: 37px;
}

.dropdown {
  top: var(--header-height);
}

37px--header-height になったことで、ヘッダーの高さに連動しているという関係が明確です。ヘッダーの高さを変更するときも :root の 1 箇所を直すだけで済みます。

ただし何でもカスタムプロパティにすればいいわけではありません。padding: 16px のような一般的なスペーシングまで全部変数化すると、定義ファイルが肥大化してかえって見通しが悪くなります。

変数化すべき値

他の要素と連動している値、複数箇所で同じ根拠から導かれる値、変更される可能性が高い値。

変数化しなくてよい値

そのコンポーネント固有の値で、他と連動しておらず、意図が文脈から明らかな値。

デザイントークンとしての変数設計

プロジェクト全体のスペーシングやフォントサイズに一貫性を持たせるために、デザイントークンとして変数を体系的に定義する方法があります。

:root {
  --spacing-xs: 4px;
  --spacing-sm: 8px;
  --spacing-md: 16px;
  --spacing-lg: 24px;
  --spacing-xl: 32px;

  --font-size-sm: 12px;
  --font-size-md: 14px;
  --font-size-lg: 16px;
  --font-size-xl: 20px;
}

この体系があれば padding: 13px のような中途半端な値が入り込みにくくなります。13px はトークンのどれにも該当しないので、レビュー時に「なぜこの値なのか」という疑問が自然に発生します。

/* トークンに従った記述 */
.card {
  padding: var(--spacing-md);
  font-size: var(--font-size-md);
}

/* マジックナンバー */
.card {
  padding: 13px;
  font-size: 15px;
}

トークンに存在しない値を使っている箇所は、デザインとの乖離かマジックナンバーのどちらかです。いずれにしても確認が必要なコードだということが、トークン体系があることで機械的に判別できるようになります。

calc() で根拠を式にする

マジックナンバーの中には、複数の値を足し引きした結果として導かれたものがあります。その場合は calc() で計算式をそのまま書くことで、根拠をコードに残せます。

/* マジックナンバー */
.sidebar {
  height: 648px;
}

/* calc() で根拠を明示 */
.sidebar {
  height: calc(100vh - var(--header-height) - var(--footer-height));
}

648px という値がビューポートの高さからヘッダーとフッターを引いた結果だったとしても、数値だけではその関係は読み取れません。calc() で式にしておけば、ヘッダーやフッターの高さが変わっても自動的に追従します。

/* 複数の根拠が絡む例 */
.tooltip {
  /* ボタンの高さ + ボタンとツールチップの間隔 */
  top: calc(var(--button-height) + var(--spacing-sm));
}

calc() の中にカスタムプロパティを入れることで、数値の由来が完全にコード上で説明されます。コメントを書かなくても式自体が意図を語ってくれる形です。

マジックナンバーを見つける方法

既存のコードからマジックナンバーを見つけるには、Stylelint を使えます。

{
  "rules": {
    "declaration-property-value-no-unknown": true
  }
}

ただし Stylelint にはマジックナンバーを直接検出するルールは組み込まれていません。stylelint-declaration-strict-value というプラグインを使うと、特定のプロパティに対して変数やキーワード以外の値を禁止できます。

{
  "plugins": [
    "stylelint-declaration-strict-value"
  ],
  "rules": {
    "scale-unlimited/declaration-strict-value": [
      ["/color$/", "font-size", "z-index"],
      {
        "ignoreValues": ["inherit", "transparent", "currentColor"]
      }
    ]
  }
}

この設定では colorfont-sizez-index に対してリテラル値を直接書くとエラーになります。変数かキーワードのみ許可されるので、マジックナンバーの混入を機械的に防げます。

どこまで徹底するか

マジックナンバーの排除を突き詰めると、すべての数値を変数にしたくなりますが、現実的にはコストとのバランスです。

厳密に排除すべきもの

色、z-index、ブレイクポイント、他の要素と連動するサイズ。これらは変数化しないと保守時に破綻しやすい。

許容してよいもの

border-radius: 4pxgap: 8px のようなコンポーネント内に閉じた値で、デザイントークンに合致しているもの。変数を経由しなくても意図は明らか。

重要なのは「なぜこの値なのか」がコードから読み取れるかどうかです。トークンの体系に沿った値であれば、変数を使わず直接書いても根拠は自明です。トークンに存在しない値、他の要素に依存する値、計算で導かれた値が、マジックナンバーとして排除すべき対象になります。