CSS calc()関数の深掘りと実践的な使い方

CSSのcalc()関数を「便利な計算関数」としてだけでなく、CSSの値解決プロセスの視点から深掘りし、実際のプロジェクトでの応用例を紹介します。

|

calc() は CSS プロパティの値に計算式を書ける関数です。width: calc(100% - 20px) のように、異なる単位を混ぜた計算ができます。 この記事ではもう少し踏み込んで、なぜ calc() が必要だったのかなぜ異なる単位間の計算が可能なのかを CSS の値解決プロセスから解き明かしていきます。後半では、実際のプロジェクトでの活用例も紹介します。

calc()の基本構文

calc() の基本形はシンプルです。

/* 異なる単位を混ぜた計算 */
width: calc(100% - 20px);

/* CSS Custom Properties との組み合わせ */
font-size: calc(var(--base-size) * 1.5);

/* ネストも可能(ただし省略しても動きます) */
width: calc(100% - calc(2rem + 10px));
width: calc(100% - (2rem + 10px));  /* これでも同じ */

四則演算が使えますが、いくつかルールがあります。

/* OK: + と - の前後にはスペースが必須 */
width: calc(100% - 20px);

/* NG: スペースがないとパースエラー */
width: calc(100% -20px);

/* OK: * と / にスペースは不要(ただし可読性のため推奨) */
font-size: calc(1rem * 1.5);

/* NG: 単位同士の乗算はできない */
width: calc(10px * 10px);

/* NG: 0での除算 */
width: calc(100px / 0);

+- の前後にスペースが必要なのは、CSS パーサーが -20px を「マイナス20ピクセル」という一つのトークンとして解釈してしまうためです。calc(100% -20px) は引き算ではなく、2つの値が並んでいる不正な式と見なされてしまいます。

使える型

calc() は長さ(<length>)だけでなく、数値型の値が期待されるほぼすべての場所で使えます。

代表的な単位使用例
<length>px, rem, vw, cqwwidth: calc(100vw - 20px)
<percentage>%width: calc(100% - 30%)
<length-percentage>px + %margin: calc(5% + 10px)
<angle>deg, rad, turnrotate(calc(45deg + 0.25turn))
<time>s, msanimation-duration: calc(var(--duration) * 2)
<number>(単位なし)opacity: calc(0.5 + 0.3)
<integer>(単位なし)z-index: calc(10 + 5)

実務で特によく使うのは <length-percentage> でしょう。%px のように、参照先が異なる単位を一つの式で扱えるのが calc() の最大の強みです。

なぜ calc() が必要だったのか

ここで一歩引いて、そもそもなぜ calc() という関数が CSS に追加される必要があったのかを考えてみましょう。

絶対値と相対値

CSS のサイズ指定に使われる値は、大きく絶対値相対値に分かれます。

/* 絶対値: コンテキストに依存せず値が確定する */
width: 200px;
padding: 20px;

/* 相対値: 参照先に応じて動的に変化する */
width: 50%;          /* 親要素の幅に依存 */
font-size: 1.5em;    /* 親のフォントサイズに依存 */
height: 100vh;       /* ビューポートの高さに依存 */

レスポンシブデザインでは、この2つを組み合わせたいケースが頻繁に生じます。たとえば「画面幅いっぱいに広げつつ、左右に固定の余白を確保したい」というような場面です。

/* やりたいこと: 親の幅100%から、左右のpadding分を引いた幅にしたい */
/* CSS 2.1 ではこれを1つの宣言で表現できなかった */

CSS 2.1 の時点では、異なる単位を組み合わせた計算式を書く構文が存在しませんでした。% の値は親のサイズが決まるまで確定しませんし、px は最初から確定しています。この値が確定するタイミングの違いが、単純な演算を許さなかったのです。

calc() はこの問題を解決するために CSS Values and Units Module Level 3 で導入されました。

なぜ異なる単位間の計算が可能なのか

calc(50% - 20px) と書いたとき、50% は親要素の幅が決まるまで具体的なピクセル値がわかりません。一方 20px は最初から確定しています。この2つをどうやって引き算するのでしょうか?

答えは、CSS の値解決プロセス(Value Processing) にあります。

CSS Value Processing の6段階

CSS プロパティの値は、宣言されてから画面に描画されるまでに6つの段階を経て決定されます。

  1. Declared Values — 要素に適用されるスタイル宣言をすべて集める
  2. Cascaded Values — 詳細度や出現順序により、各プロパティにつき最大1つに絞る
  3. Specified Values — 継承や初期値でデフォルトを補完し、全プロパティに値を入れる
  4. Computed Valuesempx などフォント相対値や currentColor を解決する
  5. Used Valuesレイアウト計算後% やビューポート相対単位を具体的な px に変換する
  6. Actual Values — サブピクセルの丸めなどハードウェア制約を適用し最終値にする

ここで重要なのは ステップ5の Used Values です。相対値を含む calc() 式は、Computed Values の段階では式のまま保持されます。レイアウトが確定して初めて、すべての値が px に揃い、そこで演算が実行されるのです。

つまり calc() の本質は、異なるタイミングで解決される値の演算を、Used Values の段階まで遅延させる仕組みだと言えます。

具体例: calc(50% - 20px) の解決過程

次のコードで、値がどのように解決されるかを追ってみましょう。

<style>
  .parent {
    width: 600px;
    padding: 20px;
  }
  .child {
    width: calc(50% - 20px);
  }
</style>
<div class="parent">
  <div class="child">この要素の幅は?</div>
</div>

.parent の値解決:

width: 600pxpadding: 20px はどちらも絶対値なので、Declared → Cascaded → Specified → Computed とそのまま進みます。Used Values の段階でレイアウトが確定し、content-box の幅は 600px - 20px × 2 = 560px になります。

.child の値解決:

段階width の値
Declared〜Computedcalc(50% - 20px) のまま保持
Used Values50% が親の content-box 幅 560px の 50% = 280px に解決
calc(280px - 20px)260px

Computed Values の段階では 50% がまだ具体的な値に解決できないため、calc() 式がそのまま保持されています。レイアウト計算後の Used Values で初めて %px に変換され、同じ単位同士の引き算として評価されます。

これが、calc() が異なる単位間の計算を可能にしている仕組みです。

実際のプロジェクトでの活用例

ここからは、このブログの実装で calc() を使っている箇所を見ていきます。

コンポーネントの比率計算 — ThemeToggle

ダークモード切り替えのトグルスイッチでは、スイッチのサイズをCSS Custom Propertyで管理し、calc() で幅やアニメーション距離を導出しています。

.toggle-switch {
  --toggle-switch-size: var(--toggle-size);

  /* 幅をスイッチの高さの1.9倍に */
  width: calc(var(--toggle-switch-size) * 1.9);
  height: var(--toggle-switch-size);
}

.toggle-switch::before {
  /* つまみの初期位置 */
  transform: scale(0.75);
}

.toggle-switch:checked::before {
  /* チェック時: つまみをスイッチ幅の約半分だけ移動 */
  transform: translateX(calc(var(--toggle-switch-size) * 0.9)) scale(0.75);
}

--toggle-switch-size という1つの変数を基準にして、幅(* 1.9)もスライド距離(* 0.9)も計算で導出しています。サイズを変更したくなったとき、変数を1箇所変えるだけですべてが連動します。

ビューポート依存の高さ制御 — TableOfContents

目次コンポーネントでは、サイドバーに sticky で固定しつつ、ビューポートの高さに応じて最大高さを制御しています。

.toc-container {
  position: sticky;
  top: 6rem;
  max-height: calc(100vh - 10rem);
  overflow-y: auto;
}

100vh(ビューポート全体の高さ)から 10rem(ヘッダーや上下の余白)を引くことで、「画面に収まる範囲で最大限の高さを確保しつつ、はみ出したらスクロールする」という挙動を実現しています。ここで max-height: 90% のような % 指定にしないのは、% だと親要素の高さが基準になるためです。親の高さがビューポートと一致しないケースでは意図した挙動になりません。100vh を使うことで、直接ビューポートを基準にした計算ができます。

デザイントークンの演算 — Hero

ヒーローセクションのレタースペーシングでは、デザイントークンを演算して負の値を生成しています。

:root {
  --hero-letter-spacing: calc(var(--size-50) / 3 * -1);
}

var(--size-50) というプリミティブトークンを 3 で割り、-1 を掛けて負のレタースペーシングにしています。デザイントークンの体系に「負のスペーシング」を追加するのではなく、既存のトークンから calc() で導出するアプローチです。

モダンCSS関数との使い分け

CSS には calc() の他にも数学関数があります。min()max()clamp() です。それぞれ役割が異なります。

/* calc(): 計算結果をそのまま使う */
width: calc(100% - 2rem);

/* min(): 2つの値のうち小さい方を採用 */
width: min(100%, 800px);

/* max(): 2つの値のうち大きい方を採用 */
width: max(300px, 50%);

/* clamp(): 最小値・推奨値・最大値で範囲を制約 */
font-size: clamp(14px, 2.5vw, 24px);

使い分けの基準はシンプルです。

  • 固定の計算をしたい → calc()
  • 範囲の制約をかけたい → clamp() / min() / max()

もちろん、組み合わせて使うこともできます。

/* clamp の中で calc を使う */
font-size: clamp(14px, calc(0.5rem + 1vw), 24px);

/* min の中で calc を使う */
width: min(calc(100% - 2rem), 800px);

clamp() が登場する以前は、calc() とメディアクエリを組み合わせてレスポンシブなフォントサイズを実現していました。現在では、値の範囲を制約したいケースは clamp() に任せ、calc() は純粋な演算に集中させるのが整理しやすいでしょう。

まとめ

calc() は単なる便利な計算関数ではなく、CSS の値解決プロセスにおいて、異なるタイミングで確定する値の演算を遅延させる仕組みです。

  • %vw のような相対値は、レイアウト計算後の Used Values の段階で初めて px に変換されます
  • calc() 式は Computed Values の段階では式のまま保持され、Used Values で評価されます
  • だからこそ、calc(100% - 20px) のような異なる単位の混合計算が可能になります

実務では、CSS Custom Properties と組み合わせることで「1つの変数からサイズ体系を導出する」「ビューポートと固定値を組み合わせたレイアウト制約」といったパターンが特に有用です。

clamp()min() / max() といったモダンな関数と役割を分担しつつ、calc() は引き続き CSS レイアウトの基盤として欠かせない存在であり続けるでしょう。