06機能対応分岐 — 対応可否を先に確認して別ルールを切り替える配置
特定の機能の対応可否を先に検査し、対応していれば新しいルールを、対応していなければ従来のルールをそのまま残す形で画面が分かれます。
使いどころグリッドレイアウトの段階的改善、新しい色関数を採用するカード
CSS.supportsaspect-ratiocontainer-type

<main class="stage">
<div class="wrap is-demo" id="wrap">
<div class="head">
<span class="modeLabel" id="modeLabel">実際のサポート判定</span>
<button class="rescanBtn" id="rescanBtn" type="button">再チェック</button>
</div>
<ul class="checkList" id="checkList"></ul>
<div class="bar"><div class="barFill" id="barFill"></div></div>
<button class="simToggle" id="simToggle" type="button" aria-pressed="false">フォールバックを再現</button>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 계산 겹은 index.html 의 scan() 에 있다 — CSS.supports() 로 실제 렌더 환경을 검사해 okCount/전체
// 비율을 구하고 그 비율을 막대 scaleX 에 그대로 반영한다(하드코딩 비율 아님). 여기는 목록 행의
// 등장 시차와 막대·아이콘 표기만 맡는다.
@include stage($stage-ink);
.wrap {
width: 240px;
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-3;
}
.head {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.modeLabel {
color: $subject-cream;
font-size: 10px;
font-weight: 700;
}
.rescanBtn {
appearance: none;
border: none;
border-radius: $r-pill;
background: $subject-blue;
color: $subject-cream;
padding: $sp-1 $sp-3;
font-size: 10px;
font-weight: 600;
cursor: pointer;
transition: transform $dur-instant $ease-out;
&:active { transform: scale(.94); }
&:focus-visible { outline: 2px solid $subject-blue-light; outline-offset: 2px; }
}
.checkList {
width: 100%;
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: $sp-1;
}
.checkRow {
display: flex;
align-items: center;
gap: $sp-2;
padding: $sp-1 $sp-2;
border-radius: $r-sm;
background: rgba($subject-cream, .06);
opacity: 0;
transform: translateY(6px);
transition: opacity $dur-base $ease-out, transform $dur-base $ease-out;
transition-delay: calc(var(--i, 0) * 55ms);
}
.checkRow.is-revealed {
opacity: 1;
transform: translateY(0);
}
.checkRow.is-hidden {
opacity: 0;
transform: translateY(6px);
transition-delay: 0ms;
}
.statusIcon {
flex: 0 0 auto;
width: 12px;
height: 12px;
border-radius: $r-pill;
background: $subject-blue-light;
}
.checkRow.is-ok .statusIcon { background: $subject-mint; }
.checkRow.is-fallback .statusIcon { background: $error-soft; }
.featureName {
color: $subject-cream;
font-size: 11px;
}
.bar {
width: 100%;
height: 6px;
border-radius: $r-pill;
background: rgba($subject-cream, .12);
overflow: hidden;
}
.barFill {
width: 100%;
height: 100%;
border-radius: $r-pill;
background: $subject-mint;
transform-origin: left center;
transform: scaleX(0);
transition: transform $dur-base $ease-out;
}
.simToggle {
appearance: none;
border: none;
border-radius: $r-pill;
background: rgba($subject-cream, .14);
color: $subject-cream;
padding: $sp-1 $sp-3;
font-size: 10px;
font-weight: 600;
cursor: pointer;
transition: transform $dur-instant $ease-out;
&:active { transform: scale(.96); }
&[aria-pressed="true"] { background: $subject-orange-deep; }
&:focus-visible { outline: 2px solid $subject-blue-light; outline-offset: 2px; }
}
@media (max-width: 340px) {
.wrap { width: 192px; gap: $sp-1; }
.head { gap: $sp-1; }
.featureName { font-size: 9px; }
.checkList { gap: 0; }
.checkRow { padding: 0 $sp-1; gap: $sp-1; }
}
@media (prefers-reduced-motion: reduce) {
.checkRow { transition: none; }
.barFill { transition: none; }
.rescanBtn, .simToggle { transition: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.wrap {
width: 240px;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.head {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.modeLabel {
color: #fff7e6;
font-size: 10px;
font-weight: 700;
}
.rescanBtn {
appearance: none;
border: none;
border-radius: 999px;
background: #2f6df6;
color: #fff7e6;
padding: 4px 12px;
font-size: 10px;
font-weight: 600;
cursor: pointer;
transition: transform 140ms ease-out;
}
.rescanBtn:active {
transform: scale(0.94);
}
.rescanBtn:focus-visible {
outline: 2px solid #5b8cf8;
outline-offset: 2px;
}
.checkList {
width: 100%;
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 4px;
}
.checkRow {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 8px;
border-radius: 8px;
background: rgba(255, 247, 230, 0.06);
opacity: 0;
transform: translateY(6px);
transition: opacity 300ms ease-out, transform 300ms ease-out;
transition-delay: calc(var(--i, 0) * 55ms);
}
.checkRow.is-revealed {
opacity: 1;
transform: translateY(0);
}
.checkRow.is-hidden {
opacity: 0;
transform: translateY(6px);
transition-delay: 0ms;
}
.statusIcon {
flex: 0 0 auto;
width: 12px;
height: 12px;
border-radius: 999px;
background: #5b8cf8;
}
.checkRow.is-ok .statusIcon {
background: #4cd4a6;
}
.checkRow.is-fallback .statusIcon {
background: #ffe0d8;
}
.featureName {
color: #fff7e6;
font-size: 11px;
}
.bar {
width: 100%;
height: 6px;
border-radius: 999px;
background: rgba(255, 247, 230, 0.12);
overflow: hidden;
}
.barFill {
width: 100%;
height: 100%;
border-radius: 999px;
background: #4cd4a6;
transform-origin: left center;
transform: scaleX(0);
transition: transform 300ms ease-out;
}
.simToggle {
appearance: none;
border: none;
border-radius: 999px;
background: rgba(255, 247, 230, 0.14);
color: #fff7e6;
padding: 4px 12px;
font-size: 10px;
font-weight: 600;
cursor: pointer;
transition: transform 140ms ease-out;
}
.simToggle:active {
transform: scale(0.96);
}
.simToggle[aria-pressed=true] {
background: #c93000;
}
.simToggle:focus-visible {
outline: 2px solid #5b8cf8;
outline-offset: 2px;
}
@media (max-width: 340px) {
.wrap {
width: 192px;
gap: 4px;
}
.head {
gap: 4px;
}
.featureName {
font-size: 9px;
}
.checkList {
gap: 0;
}
.checkRow {
padding: 0 4px;
gap: 4px;
}
}
@media (prefers-reduced-motion: reduce) {
.checkRow {
transition: none;
}
.barFill {
transition: none;
}
.rescanBtn, .simToggle {
transition: none;
}
}(function () {
var STEP_MS = 620; // 자동 재검사/시뮬레이션 間隔
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var wrap = document.getElementById('wrap');
var checkList = document.getElementById('checkList');
var barFill = document.getElementById('barFill');
var rescanBtn = document.getElementById('rescanBtn');
var simToggle = document.getElementById('simToggle');
var modeLabel = document.getElementById('modeLabel');
var isDemo = true;
function human() { if (isDemo) { wrap.classList.remove('is-demo'); isDemo = false; } }
// 실제 @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'); } }
];
var simOn = false;
var rows = [];
function build() {
checkList.innerHTML = '';
rows = FEATURES.map(function (f, i) {
var li = document.createElement('li');
li.className = 'checkRow is-hidden';
li.style.setProperty('--i', String(i));
var icon = document.createElement('span');
icon.className = 'statusIcon';
var name = document.createElement('span');
name.className = 'featureName';
name.textContent = f.label;
li.appendChild(icon);
li.appendChild(name);
checkList.appendChild(li);
return { el: li, icon: icon, ok: f.test() };
});
}
build();
function scan() {
var okCount = 0;
rows.forEach(function (row, i) {
// 시뮬레이션판: 실제 결과 위에 강제로 미지원을 덧씌워 폴백 문구가 보이게 한다(감지 흉내가 아니라 표시 전용 오버라이드)
var displayOk = simOn ? (row.ok && i % 2 === 0) : row.ok;
row.el.classList.toggle('is-ok', displayOk);
row.el.classList.toggle('is-fallback', !displayOk);
row.el.classList.remove('is-hidden');
row.el.classList.add('is-revealed');
if (displayOk) okCount += 1;
});
var ratio = rows.length ? okCount / rows.length : 0;
barFill.style.transform = 'scaleX(' + ratio + ')';
modeLabel.textContent = simOn ? 'フォールバック再現チェック' : '実際のサポート判定';
}
rescanBtn.addEventListener('click', function () {
human();
rows.forEach(function (row) { row.el.classList.add('is-hidden'); row.el.classList.remove('is-revealed'); });
requestAnimationFrame(function () { requestAnimationFrame(scan); });
});
function setSim(on) {
simOn = on;
simToggle.setAttribute('aria-pressed', String(on));
scan();
}
simToggle.addEventListener('click', function () { human(); setSim(!simOn); });
scan();
var last = 0;
var phase = 0;
function loop(ts) {
if (!last) last = ts;
if (isDemo && !reduce && ts - last >= STEP_MS) {
last = ts;
phase = (phase + 1) % 3;
if (phase === 0) {
rows.forEach(function (row) { row.el.classList.add('is-hidden'); row.el.classList.remove('is-revealed'); });
} else if (phase === 1) {
scan();
} else {
setSim(!simOn);
}
}
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
})();// SupportsQuery — vanilla 판(run/375/demo/06-compat-supports-query/index.html) 의 React 포트.
// CSS.supports() 로 실제 렌더 환경을 검사해 항목별 지원/미지원을 표시하고, 통과 비율을 막대 scaleX 로 잰다.
import { useEffect, useRef } from "react";
import styles from "./SupportsQuery.module.scss";
type Box<T> = { current: T };
const STEP_MS = 620; // 자동 재검사/시뮬레이션 간격
export default function SupportsQuery() {
const rootRef = useRef(null) as unknown as Box<HTMLDivElement>;
const checkListRef = useRef(null) as unknown as Box<HTMLUListElement>;
const barFillRef = useRef(null) as unknown as Box<HTMLDivElement>;
const rescanBtnRef = useRef(null) as unknown as Box<HTMLButtonElement>;
const simToggleRef = useRef(null) as unknown as Box<HTMLButtonElement>;
const modeLabelRef = useRef(null) as unknown as Box<HTMLSpanElement>;
const rafRef = useRef(0) as unknown as Box<number>;
useEffect(() => {
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const wrap = rootRef.current;
const checkList = checkListRef.current;
const barFill = barFillRef.current;
const rescanBtn = rescanBtnRef.current;
const simToggle = simToggleRef.current;
const modeLabel = modeLabelRef.current;
if (!wrap || !checkList || !barFill || !rescanBtn || !simToggle || !modeLabel) return;
let isDemo = true;
function human() { if (isDemo) { wrap!.classList.remove(styles.isDemo); isDemo = false; } }
// 실제 @supports 와 같은 판정 근거 — CSS.supports() 로 지금 렌더 환경을 그대로 검사한다
const FEATURES = [
{ label: "플렉스 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"); } }
];
let simOn = false;
let rows: { el: HTMLLIElement; icon: HTMLSpanElement; ok: boolean }[] = [];
function build() {
checkList!.innerHTML = "";
rows = FEATURES.map(function (f, i) {
const li = document.createElement("li");
li.className = styles.checkRow + " " + styles.isHidden;
li.style.setProperty("--i", String(i));
const icon = document.createElement("span");
icon.className = styles.statusIcon;
const name = document.createElement("span");
name.className = styles.featureName;
name.textContent = f.label;
li.appendChild(icon);
li.appendChild(name);
checkList!.appendChild(li);
return { el: li, icon: icon, ok: f.test() };
});
}
build();
function hideAll() {
rows.forEach(function (row) { row.el.classList.add(styles.isHidden); row.el.classList.remove(styles.isRevealed); });
}
function scan() {
let okCount = 0;
rows.forEach(function (row, i) {
// 시뮬레이션판: 실제 결과 위에 강제로 미지원을 덧씌워 폴백 문구가 보이게 한다(감지 흉내가 아니라 표시 전용 오버라이드)
const displayOk = simOn ? (row.ok && i % 2 === 0) : row.ok;
row.el.classList.toggle(styles.isOk, displayOk);
row.el.classList.toggle(styles.isFallback, !displayOk);
row.el.classList.remove(styles.isHidden);
row.el.classList.add(styles.isRevealed);
if (displayOk) okCount += 1;
});
const ratio = rows.length ? okCount / rows.length : 0;
barFill!.style.transform = "scaleX(" + ratio + ")";
modeLabel!.textContent = simOn ? "폴백 흉내 검사" : "실제 지원 검사";
}
function onRescan() {
human();
hideAll();
requestAnimationFrame(function () { requestAnimationFrame(scan); });
}
rescanBtn.addEventListener("click", onRescan);
function setSim(on: boolean) {
simOn = on;
simToggle!.setAttribute("aria-pressed", String(on));
scan();
}
function onSimClick() { human(); setSim(!simOn); }
simToggle.addEventListener("click", onSimClick);
scan();
let last = 0;
let phase = 0;
function loop(ts: number) {
if (!last) last = ts;
if (isDemo && !reduce && ts - last >= STEP_MS) {
last = ts;
phase = (phase + 1) % 3;
if (phase === 0) {
hideAll();
} else if (phase === 1) {
scan();
} else {
setSim(!simOn);
}
}
rafRef.current = requestAnimationFrame(loop);
}
rafRef.current = requestAnimationFrame(loop);
return () => {
rescanBtn!.removeEventListener("click", onRescan);
simToggle!.removeEventListener("click", onSimClick);
cancelAnimationFrame(rafRef.current);
};
}, []);
return (
<main className={styles.stage}>
<div className={styles.wrap + " " + styles.isDemo} ref={rootRef}>
<div className={styles.head}>
<span className={styles.modeLabel} ref={modeLabelRef}>실제 지원 검사</span>
<button className={styles.rescanBtn} ref={rescanBtnRef} type="button">다시 검사</button>
</div>
<ul className={styles.checkList} ref={checkListRef}></ul>
<div className={styles.bar}><div className={styles.barFill} ref={barFillRef}></div></div>
<button className={styles.simToggle} ref={simToggleRef} type="button" aria-pressed="false">폴백 흉내내기</button>
</div>
</main>
);
}06 機能対応分岐 — 対応可否を先に確認して別ルールを切り替える配置 — 計測 累積面積 21.491% · 動いたフレーム 10/23GODRICH オリジナル · SCSS と React は zip に同梱 · css ブラウザ対応 9選 — 実務で使うフォールバック集
このデモが属する記事
css ブラウザ対応 9選 — 実務で使うフォールバック集9つのcss ブラウザ対応トリックのうち、1つはアドレスバーの開閉のたびに画面全体の高さを再計算し、1つは入力欄にフォーカスしても画面が拡大されないようロックし、1つはリストを端まで引っ張っても背景が一緒に揺れないよう止めます。 この記事のデモをすべて見る

