CSSのclamp()、便利だってことは分かってるんですが、最小値・最大値・可変部分の計算式を毎回電卓片手に手計算するの、本当にだるいです。
/* 最小16px、最大32px、その間は画面幅に応じて滑らかに変化 */
font-size: clamp(16px, 4vw + 1rem, 32px);
この真ん中の「4vw + 1rem」みたいな可変部分の係数を、VW単位がどうとか公式をコピペしてくるたびに「これで合ってるよな…?」と不安になっていました。Clamp Scopeは、その不安と面倒くささをまとめて消したくて作った道具です。
やったことはシンプルで、最小サイズ・最大サイズ・それぞれが適用される画面幅を、数値入力じゃなくスライダーで直感的に動かせるようにしました。動かした瞬間にプレビューのテキストサイズがヌルッと変わるので、計算式のことなんて一切考えずに「このくらいの感じ」で決められます。
ここは触った瞬間の気持ちよさにかなりこだわりました。
レスポンシブに対応したスムーズなサイト作りには欠かせない重要な機能なので、より簡単により使いやすくを目指しました。
で、感覚で決めたあとは、最終的にコピペしてそのまま使えるclamp()のコードを正確に出す。ここだけは絶対に計算ミスがあってはいけない部分なので、地味に一番テストに時間をかけました。
見た目のスライダーがいくら気持ちよくても、出てくるコードが間違ってたら道具として本末転倒ですからね。
メディアクエリを何個も書かなくていいというのがclamp()の一番の強みだと思うので、その恩恵をもっと気軽に受けてもらえたら、というのが正直な願いです。
clamp()の計算式は、1次関数の傾きと切片から算出
やっていることは、Utopiaなどでも使われている標準的な一次関数(線形補間)です。コード上はformulaというuseMemoで計算しています。
const slope = (maxValuePx - minValuePx) / (maxVw - minVw); // px単位の傾き
const slopeVw = slope * 100; // 傾きをvw単位に変換
const interceptPx = minValuePx - slope * minVw; // 切片(px)
const interceptRem = interceptPx / 16; // 切片をremに変換
考え方としては、「画面幅(vw)を横軸、値(px)を縦軸」とした一次関数の傾きと切片を求めているだけです。傾きは1vw(=ビューポート幅の1%)あたり何px変化するかなので×100、切片はrem変換のため÷16(ブラウザの既定フォントサイズ16px前提)しています。
clamp(下限rem, 切片rem ± 係数vw, 上限rem)という形に組み立てて、下限・上限はMath.min/Math.maxでソートしてから渡しています。
vw単位のアクセシビリティ懸念は、業界標準の手法をそのまま踏襲
正直に言うと、追加の対策はしていません。やっていることとしては、上限・下限を必ずremで固定しているので、「vw部分がどれだけ暴れても、最終的な値は指定したmin〜maxの範囲を超えない」というフロア・シーリングは効いています。これは最低限の安全網です。
ただ、ブラウザの「文字サイズのみ拡大」機能(古い一部ブラウザのtext-only zoom)に対してvwは反応しないという問題自体は、この手法固有の限界であり、ツール側で解決できるものではありません(一般的なページ全体のズーム(Ctrl+/-)はvwも含めて拡大されるので問題ありません)。
本来やるべき対策としては、「vwの代わりにcqw(コンテナクエリ単位)を使う」オプションを用意する、あるいは生成コードの下に「WCAG 1.4.4 Resize textの観点で、極端に小さいmin値は避けてください」といった注意書きを添える、といった方向性かなと思っています。今は入れていないので、必要であれば追加できます。
あり得ない入力値のバリデーションは、項目によって扱いが違う
画面幅(min/max viewport)はmaxVw > minVwをvalidフラグでチェックしています。
const valid = maxVw > minVw;
falseの場合は赤字で警告を出し、formulaはnullを返し、コピーボタンも無効化しています。ゼロ除算(maxVw - minVwが0)もこれで同時に防いでいます。
一方、値(min/max value)はあえてバリデーションしていません。「画面が広くなるほど値が小さくなる」という使い方(例:スマホでは余白を広めに、デスクトップでは余白を狭めに、といったケース)が実際にあり得るからです。傾きがマイナスになるだけで計算上は問題なく、clamp()の引数だけMath.min/Math.maxでソートして辻褄を合わせています。
「テストで苦労した」ではなく、設計で詰めておいた結果…
このツールは、人間の開発者のように「書いて→ブラウザで動かして→バグを見つけて→直す」というサイクルを繰り返して作ったものではなく、コードを書く時点で一括生成しています。それでも、実際に起きたこと・書きながら意識して潰した個所はというと…。
実際にユーザー環境で見つかって直したもの(これは後から発覚した不具合です)
- Viteのデフォルト
App.css/index.cssが#rootの幅や中央寄せを制限していて、ツールが画面幅いっぱいに表示されなかった - 「自動で動かす」ボタンが目立たず、押せることが伝わりにくかった
- プレビュー文字がグラフから離れすぎていて、ドラッグ操作と文字サイズの変化を同時に確認できなかった
コードを書く段階で意識して先回りしたもの(テストで踏んだわけではなく、設計時点で対策済みのもの)
| 想定した問題 | 対策 |
|---|---|
minVw === maxVwのゼロ除算 | 上記のvalidフラグで事前に弾く |
| min値とmax値が同じ(傾き0)のときグラフのY軸レンジが潰れる | (yHi - yLo) * 0.35 || 10でフォールバック値を用意 |
| ドラッグ中にマウスがSVG領域の外に出ると追従しなくなる | svg要素ではなくwindowにpointermove/pointerupを貼る |
| min/maxの画面幅を後から変更すると、ドラッグ済みのプローブ位置が範囲外に取り残される | useEffectで都度クランプし直す |
「苦労して見つけたバグです」と言えるほどの体験談は正直なく、実際に手を動かして触っていただいたことで見つかった前者3つが、一番リアルな“開発中の気づき”です。
可変レスポンシブなどに便利です、とりあえず一度試していただけたらと思います。👇
