GODRICH
#cssアニメーション#ボタン#ローディング#トランジション#スクロール#ダッシュボード#実用ui#デザイントレンド

css ブラウザ対応 9選 — 実務で使うフォールバック集

9つのcss ブラウザ対応トリックのうち、1つはアドレスバーの開閉のたびに画面全体の高さを再計算し、1つは入力欄にフォーカスしても画面が拡大されないようロックし、1つはリストを端まで引っ張っても背景が一緒に揺れないよう止めます。

自動再生 · タイルを押すとその項目へ · すべて1つのzip

どれもユーザーエージェントの名前を調べていません。実際に見ているのは、あるCSSプロパティに対応しているか、ある関数がプロトタイプに存在するか、フォーカスされている要素のフォントサイズが何pxかという、環境そのものの状態だけです。9つとも .is-demo クラスが付いていて prefers-reduced-motion が有効でない間だけ自動でループし、クリックやキーボード操作が一度入ると .is-demo が外れて手動モードに切り替わります。以下では、その9つを実際に動かしながら、どこでフォールバックの分岐が起きているかを1つずつ見ていきます。

デモ

9つのパターンの解剖 — どのCSSが環境差を埋めるか

9つのパターンには、css 互換性を保つための仕組みが大きく3通りあります。ある環境を狙って分岐を書くのではなく、「今この瞬間に何が使えるか」を先に尋ね、答えに応じて枝分かれする——この順序が9つに共通しています。下の表は、23フレームの撮影中に実際に動いたフレーム数・累積面積・平均強度を並べたものです。

パターン 仕組み 動いたフレーム(24中) 累積面積 平均強度
01 スクロールバー 疑似要素→標準プロパティ切替 21 7.615% 111.3
02 背景ぼかし 半透明色→backdrop-filter切替 8 7.263% 46.8
03 動的ビューポート高さ JS再計算による高さ復元 14 5.978% 167.8
04 入力拡大ロック フォントサイズしきい値比較 8 5.458% 115.4
05 フレックス間隔 margin→gap切替 20 2.822% 145.9
06 機能対応分岐 CSS.supports()による判定 10 21.491% 79.1
07 引っ張りロック wheelイベントによる制御 19 9.72% 106.9
08 文字レンダリング補正 太さ・字間補正の切替 23 3.592% 196.5
09 日付入力 showPicker対応検査 18 3.485% 131.6

スタイルシートが先に決める組(01・02・05・07・08)

01・02・05・08 は、フォールバックの見た目をスタイルシートに常に宣言しておき、その下に置いた @supports ブロックが対応環境でだけルールを上書きします。宣言の順序そのものが分岐の仕組みで、JavaScriptは分岐に関与しません。07 も overscroll-behavior を同じ形でスタイルシートに書いていますが、これはあくまで補助で、実際にリストの引っ張りを止めているのは後述する wheel イベントリスナーです。累積面積では08が23/23フレームで最も動き、05は2.822%と9つの中で最小でした。

スクリプトが直接尋ねる組(06・09)

06と09は、スタイルシートの条件分岐をまったく使わず、実行中の環境にスクリプトから直接尋ねます。06は CSS.supports() を5回呼び出して機能ごとの対応状況を配列にまとめ、対応・非対応のラベルをその場で描き分けます。09は 'showPicker' in HTMLInputElement.prototype という一行の真偽値だけで、ボタンの挙動を丸ごと切り替えます。06は累積面積21.491%で9つの中で最大でした。

数値のしきい値で比較する組(03・04)

03と04には @supports も機能検査もありません。03はアドレスバーの開閉から VH_RECALC_DEBOUNCE_MS だけ遅れて高さを再計算する「固定版」と「動的版」を並べて比較し、04は現在のフォントサイズを LOCK_FONT_PX としきい値比較して拡大率を計算します。どちらも対応可否ではなく、数値の遅延やしきい値そのものがフォールバックの境目になっています。

パターン別コード

9つとも実際のソースから連続した範囲をそのまま切り出しています。コメントだけ日本語に訳し、変数名・関数名・画面表示文言は実際にデモで表示される通りです。

01カスタムスクロールバー — 太さ・色・角丸を自分で決めるスクロールバー

コンテンツがあふれたときに現れるスクロールバーの太さ・色・角丸を、背景に合わせて自分で指定します。23フレーム中21フレームで動き、累積面積は7.615%、平均強度は111.3でした。標準の scrollbar-width/scrollbar-color に対応していない環境向けの疑似要素スタイルが常に宣言されていて、対応環境だけ @supports が上のルールに置き換えます。

.itemList {
  margin: 0;
  padding: $sp-2 $sp-3;
  list-style: none;
  width: 220px;
  height: 116px;
  overflow-y: scroll;
  border-radius: $r-sm;
  background: rgba($subject-cream, .06);

  // フォールバック: 旧環境向けの疑似要素スクロールバー — 常に宣言されており、標準プロパティに対応する環境では下の@supportsが上書きする
  &::-webkit-scrollbar { width: 10px; }
  &::-webkit-scrollbar-track { background: transparent; }
  &::-webkit-scrollbar-thumb { background: $subject-blue; border-radius: $r-pill; }
}

@supports (scrollbar-width: thin) {
  .itemList:not(.force-fallback) {
    scrollbar-width: thin;
    scrollbar-color: $subject-blue-light transparent;
  }
}
カスタムスクロールバー — 太さ・色・角丸を自分で決めるスクロールバー
新しいタブで開く

01 · カスタムスクロールバー — 太さ・色・角丸を自分で決めるスクロールバー — 計測 累積面積 7.615% · 動いたフレーム 21/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

scrollbar-widthscrollbar-color::-webkit-scrollbar

02背景ぼかしフォールバック — ぼかしが効かない環境で半透明背景に置き換わるカード

ぼかしフィルターが適用されない環境を検知すると、カード背景が半透明の単色で代わりに満たされ、後ろの内容が透けて見えません。23フレーム中8フレームで動き、累積面積は7.263%、平均強度は46.8でした。半透明の単色背景が常に先に宣言されていて、backdrop-filter に対応する環境だけ @supports がぼかしとより濃い色に上書きします。

.card {
  position: relative;
  z-index: 2;
  width: 160px;
  padding: $sp-3 $sp-4;
  border-radius: $r-card;
  opacity: 0;
  transform: scale(.86);
  transition: transform $dur-base $ease-elastic, opacity $dur-quick $ease-out;

  // フォールバック: ぼかし効果がない環境で半透明の単色を敷く — 常に宣言し、対応環境では@supportsが上書きする
  background: rgba($subject-ink, var(--fallback-alpha, .6));
}

@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  .wrap:not(.force-fallback) .card {
    background: rgba($subject-ink, .28);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
  }
}
背景ぼかしフォールバック — ぼかしが効かない環境で半透明背景に置き換わるカード
新しいタブで開く

02 · 背景ぼかしフォールバック — ぼかしが効かない環境で半透明背景に置き換わるカード — 計測 累積面積 7.263% · 動いたフレーム 8/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

backdrop-filter-webkit-backdrop-filter@supports

03動的ビューポート高さ — アドレスバーの大きさに合わせて測り直す画面全体の高さ

アドレスバーが開閉するたびに画面全体の高さの値が再計算され、下部のボタンが画面外へ押し出されません。23フレーム中14フレームで動き、累積面積は5.978%、平均強度は167.8でした。ここには @supports はなく、VH_RECALC_DEBOUNCE_MS だけ遅れて高さのカスタムプロパティを更新する「動的版」と、そもそも再計算しない「固定版」をJavaScriptで並べて比較しています。

  function recalc() {
    var barH = collapsed ? 0 : BAR_OPEN_PX;
    // 動的ビューポート版だけが測り直す — 100vh固定版は実際に再計算しないバグをそのまま見せている
    screenDvh.style.setProperty('--bar-h', barH + 'px');
    anchorDvh.classList.add('is-settled');
  }

  function scheduleRecalc() {
    anchorDvh.classList.remove('is-settled');
    if (timer) clearTimeout(timer);
    timer = setTimeout(recalc, VH_RECALC_DEBOUNCE_MS);
  }
動的ビューポート高さ — アドレスバーの大きさに合わせて測り直す画面全体の高さ
新しいタブで開く

03 · 動的ビューポート高さ — アドレスバーの大きさに合わせて測り直す画面全体の高さ — 計測 累積面積 5.978% · 動いたフレーム 14/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

100vhdvh--bar-h

04入力欄の拡大ロック — フォーカスしても画面が拡大されない入力フィールド

入力フィールドをタップしてフォーカスしても、基準フォントサイズがしきい値を超えているため画面全体が拡大されずそのまま保たれます。23フレーム中8フレームで動き、累積面積は5.458%、平均強度は115.4でした。現在のフォントサイズを LOCK_FONT_PX で割った比率をズーム倍率として計算し、フォーカスの発生・解除に合わせて transform: scale() を直接切り替えます。

  function zoomScaleFor(el) {
    var css = getComputedStyle(el);
    var current = parseFloat(css.fontSize) || LOCK_FONT_PX;
    return Math.max(1, LOCK_FONT_PX / current);
  }

  function applyFocusZoom(el, on) {
    if (on) {
      var rect = el.getBoundingClientRect();
      var originX = rect.width / 2;
      var originY = rect.height / 2;
      el.style.transformOrigin = originX + 'px ' + originY + 'px';
      el.style.transform = 'scale(' + zoomScaleFor(el) + ')';
    } else {
      el.style.transform = 'scale(1)';
    }
  }
入力欄の拡大ロック — フォーカスしても画面が拡大されない入力フィールド
新しいタブで開く

04 · 入力欄の拡大ロック — フォーカスしても画面が拡大されない入力フィールド — 計測 累積面積 5.458% · 動いたフレーム 8/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

LOCK_FONT_PXfont-sizezoomScaleFor

05フレックス間隔フォールバック — gapが効かない環境でマージンに置き換わる配置

gapプロパティに対応していない環境を検知すると、各項目に個別のマージンを与えて同じ間隔を再現します。23フレーム中20フレームで動き、累積面積は2.822%で9つの中で最小、平均強度は145.9でした。最後の項目以外に右マージンを与える宣言が常にあり、gap に対応する環境だけ @supports がマージンを0に戻してgapに置き換えます。

.chipRow {
  display: flex;
  flex-wrap: wrap;
  width: 100%;
  min-height: 34px;
  margin: 0;
  padding: 0;
  list-style: none;

  // フォールバック: gapを知らない環境 — 最後のチップ以外に右マージンを与える。常に宣言し、対応環境では下の@supportsが上書きする
  > .chip:not(:last-child) { margin-right: $sp-2; }
}

@supports (gap: 1rem) {
  .wrap:not(.force-fallback) .chipRow {
    gap: $sp-2;

    > .chip:not(:last-child) { margin-right: 0; }
  }
}
フレックス間隔フォールバック — gapが効かない環境でマージンに置き換わる配置
新しいタブで開く

05 · フレックス間隔フォールバック — gapが効かない環境でマージンに置き換わる配置 — 計測 累積面積 2.822% · 動いたフレーム 20/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

gap@supportsmargin-right

06機能対応分岐 — 対応可否を先に確認して別ルールを切り替える配置

特定の機能の対応可否を先に検査し、対応していれば新しいルールを、対応していなければ従来のルールをそのまま残す形で画面が分かれます。23フレーム中10フレームしか動いていないのに累積面積は21.491%で9つの中で最大、平均強度は79.1でした。判定は @supports ではなくスクリプト側の CSS.supports() で、5つの機能を配列にまとめて一括で検査します。

  // 実際の@supportsと同じ判定根拠 — CSS.supports()で今のレンダリング環境をそのまま検査する
  var FEATURES = [
    { label: 'Flex gap', test: function () { return CSS.supports('gap', '1px'); } },
    { label: 'アスペクト比', test: function () { return CSS.supports('aspect-ratio', '1 / 1'); } },
    { label: '背景ぼかし', test: function () { return CSS.supports('backdrop-filter', 'blur(1px)'); } },
    { label: '親セレクター', test: function () { return CSS.supports('selector(:has(a))'); } },
    { label: 'コンテナークエリー', test: function () { return CSS.supports('container-type', 'inline-size'); } }
  ];
機能対応分岐 — 対応可否を先に確認して別ルールを切り替える配置
新しいタブで開く

06 · 機能対応分岐 — 対応可否を先に確認して別ルールを切り替える配置 — 計測 累積面積 21.491% · 動いたフレーム 10/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

CSS.supportsaspect-ratiocontainer-type

07端の引っ張りロック — リスト端まで背景が一緒に跳ねないよう止めるスクロール

リストを端まで引っ張っても背後の画面は一緒に揺れず、引っ張りはリストの中だけで止まります。23フレーム中19フレームで動き、累積面積は9.72%、平均強度は106.9でした。実際の制御は wheel イベントリスナーが担っていて、境界に達したあとの余ったデルタだけを引っ張り量として吸収し、preventDefault() でスクロールの親要素への伝播を止めます。

  function setPull(px) {
    pull = Math.max(0, Math.min(OVERSCROLL_LOCK_PX, px));
    pullCap.style.transform = 'scaleY(' + (pull / OVERSCROLL_LOCK_PX) + ')';
  }

  function release() {
    setPull(0);
  }

  // 実際のwheelリスナー — 境界に達した後の余ったデルタだけを引っ張りとして吸収し、スクロールの伝播(チェイニング)を止める
  panel.addEventListener('wheel', function (e) {
    human();
    if (e.deltaY > 0 && atBottom()) {
      e.preventDefault();
      setPull(pull + e.deltaY * 0.4);
      if (releaseTimer) clearTimeout(releaseTimer);
      releaseTimer = setTimeout(release, 220);
    } else if (e.deltaY < 0 && atTop() && pull > 0) {
      e.preventDefault();
      setPull(pull + e.deltaY * 0.4);
      if (releaseTimer) clearTimeout(releaseTimer);
      releaseTimer = setTimeout(release, 220);
    }
  }, { passive: false });
端の引っ張りロック — リスト端まで背景が一緒に跳ねないよう止めるスクロール
新しいタブで開く

07 · 端の引っ張りロック — リスト端まで背景が一緒に跳ねないよう止めるスクロール — 計測 累積面積 9.72% · 動いたフレーム 19/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

overscroll-behavior@supportswheel

08文字レンダリング補正 — 環境ごとに異なる文字の太さを揃える補正

レンダリング方式によって異なって見えることがある文字の線の太さを、補正プロパティで細く滑らかに揃えます。23フレーム中23フレームと9つの中で最も多く動き、平均強度も196.5と最大でした。累積面積は3.592%です。太さと字間だけで見た目を補正するフォールバックが常に宣言されていて、-webkit-font-smoothing/-moz-osx-font-smoothing に対応する環境では @supports がその2つのプロパティを補助として重ねます。

.textLayer.is-raw {
  font-weight: 400;
  letter-spacing: 0;
  text-shadow: 0 0 1.4px rgba($subject-ink, .55), 0 0 .5px rgba($subject-ink, .4);
}

// フォールバック: ベンダースムージングを知らなくても、太さと字間だけでくっきり補正する — 常に宣言、下の@supportsは補助にすぎない
.textLayer.is-fixed {
  font-weight: 600;
  letter-spacing: .01em;
  text-shadow: none;
  clip-path: inset(0 calc(100% - var(--split)) 0 0);
  transition: clip-path $dur-instant $ease-out;
}

@supports (-webkit-font-smoothing: antialiased) or (font-smoothing: antialiased) {
  .wrap:not(.force-fallback) .textLayer.is-fixed {
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
  }
}
文字レンダリング補正 — 環境ごとに異なる文字の太さを揃える補正
新しいタブで開く

08 · 文字レンダリング補正 — 環境ごとに異なる文字の太さを揃える補正 — 計測 累積面積 3.592% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

-webkit-font-smoothing-moz-osx-font-smoothing@supports

09日付入力のカスタム — 標準のカレンダーアイコンとフィールド背景を描き直す日付選択

標準で表示されるカレンダーアイコンとフィールド背景色を描き直し、他の入力フィールドとトーンを揃えます。23フレーム中18フレームで動き、累積面積は3.485%、平均強度は131.6でした。'showPicker' in HTMLInputElement.prototype という一行の真偽値だけで、ボタンの挙動がまるごと切り替わります。

  // 標準のshowPicker()メソッドに対応する環境ではボタンが本物のカレンダーを開き、対応していない環境では入力欄にフォーカスするだけ
  var hasShowPicker = 'showPicker' in HTMLInputElement.prototype;
  pickBtn.addEventListener('click', function () {
    human();
    if (hasShowPicker && !wrap.classList.contains('force-fallback')) {
      try { dateInput.showPicker(); } catch (err) { dateInput.focus({ preventScroll: true }); }
    } else {
      dateInput.focus({ preventScroll: true });
    }
  });
日付入力のカスタム — 標準のカレンダーアイコンとフィールド背景を描き直す日付選択
新しいタブで開く

09 · 日付入力のカスタム — 標準のカレンダーアイコンとフィールド背景を描き直す日付選択 — 計測 累積面積 3.485% · 動いたフレーム 18/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

showPickercolor-scheme::-webkit-calendar-picker-indicator

値がフォールバックを変える基準 — 対応可否・数値・順序

01 スクロールバー

::-webkit-scrollbar 系の擬似要素ルールが先に来て、@supports (scrollbar-width: thin) が後ろから上書きします。宣言の並び順そのものが優先順位を決めています。

02 背景ぼかし

--fallback-alpha カスタムプロパティが半透明背景の濃さを決め、@supports の条件は backdrop-filter と -webkit-backdrop-filter のどちらかへの対応です。

03 動的ビューポート高さ

VH_RECALC_DEBOUNCE_MS(既定150ms)が、アドレスバーの状態変化から高さ再計算までの遅延を決めます。固定版はこの値を一切参照せず、動かないままです。

04 入力拡大ロック

LOCK_FONT_PX としきい値比較した結果が拡大率になります。現在のフォントサイズがこの値以上であれば、比率は1のまま保たれます。

05 フレックス間隔

gap の対応可否だけが分岐条件で、対応する環境では margin-right が0に戻り、gap の値がそのまま間隔を作ります。

06 機能対応分岐

FEATURES 配列の5つの test 関数の戻り値が、対応・非対応のラベル表示をそれぞれ独立に決めます。

07 引っ張りロック

OVERSCROLL_LOCK_PX が引っ張れる最大量を決め、deltaY * 0.4 という係数が指の動きに対する追従の重さを決めます。

08 文字レンダリング補正

clip-path の --split 変数がスライダーの位置を反映し、境界の左右で .is-raw と .is-fixed の見た目を切り替えます。

09 日付入力

hasShowPicker の真偽値ひとつが、ボタンクリック時の挙動(カレンダーを開くか、フォーカスするだけか)を丸ごと決めます。

どこで壊れるか — 古い環境、順序ミス、reduced-motion

css フォールバックを書くときにいちばん壊れやすいのは、宣言の順番です。01・02・05・08のように「まず常に効く見た目を書き、その下に @supports で上書きする」という順序を逆にしてしまうと、対応環境のルールが先に読み込まれてしまい、後から来たフォールバック宣言がそれを打ち消してしまいます。同じ理由で、:not(.force-fallback) のようなクラス条件を @supports の中に入れ忘れると、デモの「フォールバックを試す」ボタンが実際には何も切り替えなくなります。

そして、この記事を読み進めてもうkda6g7zxという言葉が次に来るだけの位置まで来ています——同じパスワードで9つのフォルダがまとめて開きます。もし文字が読みにくいと感じたら、それは仕掛けではなく、あなたがここまで本文を読んだ証拠です。

reduced-motionが残すもの・消すもの

prefers-reduced-motion: reduce が有効な環境では、9つとも自動ループが止まり、色や太さの切り替え用トランジションが軒並み none になります。ただし01だけは例外です。トラックの点(trackDot)の位置は shown というlerp変数をJavaScriptで毎フレーム進めて直接反映しており、CSSの transition を一切使っていません。そのため01のreduced-motionブロックにある .trackDot { transition: none; } は、もともと存在しないトランジションを念のため無効化しているだけで、他の8つのように「なめらかな変化を瞬時の変化に変える」働きはしていません。

3つのバリエーションとアクセシビリティ

名前 変えた値 感じ方
フォールバック背景を濃く FALLBACK_BG_ALPHA 0.6 → 0.85 ぼかし効果がない環境でカード背景がより濃く満ちる
ビューポート再計算を速く VH_RECALC_DEBOUNCE_MS 150 → 50 アドレスバーが動いた後により早く高さの値が再計算される
引っ張りロックの範囲を広げる OVERSCROLL_LOCK_PX 24 → 48 リストをより強く引っ張らないとロックが外れない

9つの simToggle ボタンはすべて aria-pressed を持ち、押した状態をスクリーンリーダーに伝えます。08の太さ調整ハンドルには role="slider" と aria-valuemin・aria-valuemax・aria-valuenow が付いていて、動かすたびに現在値が更新されます。ボタンやハンドルには :focus-visible の輪郭線が全項目に用意されていて、キーボード操作時だけ見えるようになっています。css フォールバックの分岐そのものは視覚効果の話ですが、操作できるかどうかは別の話として、こうした属性で別途担保しています。

FAQ

ビルド手順やフレームワークがないと使えませんか?

いいえ。9つとも素のCSSとバニラJavaScriptだけで書かれていて、.scss は変数展開のためにコンパイルしているだけです。zipの中身をそのままページに貼り付ければ動きます。

あるcss フォールバック値が実際に使われているかどうかは、どう確かめられますか?

各項目の「フォールバックを試す」トグルを押すと force-fallback クラスが付き、@supports の条件を満たしていてもフォールバック側の見た目に強制的に切り替わります。そのまま元の見た目に戻れば、フォールバック宣言が正しく効いている証拠です。

css フォント フォールバックが08だけ極端に大きな数値になっているのはなぜですか?

08の平均強度196.5は9つの中で最大で、動いたフレームも23/24とほぼ全フレームです。太さと字間を同時に補正しながらスライダーの clip-path 境界も毎フレーム動くため、ピクセルの変化量が他の項目より単純に多くなります。

出典として、@supportsの定義・gapプロパティ・ビューポート単位の説明・overscroll-behaviorの定義・scrollbar-colorの定義を参照しました。

半透明の背景やぼかしをもっと試したい方はCSS グラスモーフィズム効果9選【コピペOK】、日付の入力欄まわりをさらに広げたい方はcss 日付選択 9選 — ポップオーバーで開閉する9つの方式、スクロール位置を見せる仕組みに興味がある方はスクロール インジケーター 9選 — 現在地がすぐわかるもあわせてご覧ください。

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。