02背景ぼかしフォールバック — ぼかしが効かない環境で半透明背景に置き換わるカード
ぼかしフィルターが適用されない環境を検知すると、カード背景が半透明の単色で代わりに満たされ、後ろの内容が透けて見えません。
使いどころモーダルオーバーレイカード、通知バナーの背景
backdrop-filter-webkit-backdrop-filter@supports

<main class="stage">
<div class="wrap is-demo" id="wrap">
<div class="backdrop"></div>
<button class="openBtn" id="openBtn" type="button" aria-pressed="false">カードを開く</button>
<div class="card" id="card">
<p class="cardText">通知3件</p>
</div>
<div class="foot">
<span class="modeLabel" id="modeLabel">ぼかし対応版</span>
<button class="simToggle" id="simToggle" type="button" aria-pressed="false">フォールバックを再現</button>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 계산 겹은 카드 열림 애니메이션의 스프링 오버슛(scale 0.86→1.05→1) 에 있다 — index.html 은
// FALLBACK_BG_ALPHA 로 폴백 배경의 진하기만 넘긴다. 여기는 두 배경 표기법(흐림 · 반투명 단색)과
// 오버슛 곡선만 맡는다.
@include stage($stage-yellow);
.wrap {
position: relative;
width: 240px;
height: 150px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: $sp-3;
}
.backdrop {
position: absolute;
inset: 0;
border-radius: $r-card;
background:
radial-gradient(circle at 20% 30%, rgba($subject-pink, .9), transparent 55%),
radial-gradient(circle at 80% 70%, rgba($subject-sky, .9), transparent 55%),
radial-gradient(circle at 50% 50%, rgba($subject-lilac, .85), transparent 60%);
}
.openBtn {
position: relative;
z-index: 2;
appearance: none;
border: none;
border-radius: $r-pill;
background: $subject-ink;
color: $subject-cream;
padding: $sp-1 $sp-4;
font-size: 11px;
font-weight: 600;
cursor: pointer;
transition: transform $dur-instant $ease-out;
&:hover { transform: translateY(-1px); }
&:active { transform: scale(.95); }
&:focus-visible { outline: 2px solid $subject-blue-light; outline-offset: 2px; }
}
.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);
}
}
.card.is-open {
opacity: 1;
transform: scale(1);
}
.cardText {
margin: 0;
color: $subject-cream;
font-size: 13px;
font-weight: 600;
text-align: center;
}
.foot {
position: relative;
z-index: 2;
display: flex;
align-items: center;
gap: $sp-2;
}
.modeLabel {
color: $subject-ink;
font-size: 10px;
font-weight: 700;
background: rgba($stage-paper, .82);
padding: $sp-1 $sp-2;
border-radius: $r-xs;
}
.simToggle {
appearance: none;
border: none;
border-radius: $r-pill;
background: rgba($subject-ink, .82);
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: 200px; height: 130px; }
.card { width: 140px; }
}
@media (prefers-reduced-motion: reduce) {
.card { transition: none; }
.openBtn, .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: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.wrap {
position: relative;
width: 240px;
height: 150px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
}
.backdrop {
position: absolute;
inset: 0;
border-radius: 16px;
background: radial-gradient(circle at 20% 30%, rgba(255, 158, 187, 0.9), transparent 55%), radial-gradient(circle at 80% 70%, rgba(110, 200, 255, 0.9), transparent 55%), radial-gradient(circle at 50% 50%, rgba(185, 160, 255, 0.85), transparent 60%);
}
.openBtn {
position: relative;
z-index: 2;
appearance: none;
border: none;
border-radius: 999px;
background: #17141a;
color: #fff7e6;
padding: 4px 16px;
font-size: 11px;
font-weight: 600;
cursor: pointer;
transition: transform 140ms ease-out;
}
.openBtn:hover {
transform: translateY(-1px);
}
.openBtn:active {
transform: scale(0.95);
}
.openBtn:focus-visible {
outline: 2px solid #5b8cf8;
outline-offset: 2px;
}
.card {
position: relative;
z-index: 2;
width: 160px;
padding: 12px 16px;
border-radius: 16px;
opacity: 0;
transform: scale(0.86);
transition: transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1), opacity 220ms ease-out;
background: rgba(23, 20, 26, var(--fallback-alpha, 0.6));
}
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
.wrap:not(.force-fallback) .card {
background: rgba(23, 20, 26, 0.28);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
}
.card.is-open {
opacity: 1;
transform: scale(1);
}
.cardText {
margin: 0;
color: #fff7e6;
font-size: 13px;
font-weight: 600;
text-align: center;
}
.foot {
position: relative;
z-index: 2;
display: flex;
align-items: center;
gap: 8px;
}
.modeLabel {
color: #17141a;
font-size: 10px;
font-weight: 700;
background: rgba(255, 255, 255, 0.82);
padding: 4px 8px;
border-radius: 4px;
}
.simToggle {
appearance: none;
border: none;
border-radius: 999px;
background: rgba(23, 20, 26, 0.82);
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: 200px;
height: 130px;
}
.card {
width: 140px;
}
}
@media (prefers-reduced-motion: reduce) {
.card {
transition: none;
}
.openBtn, .simToggle {
transition: none;
}
}(function () {
var FALLBACK_BG_ALPHA = 0.6; // 흐림이 없는 환경에서 카드 배경을 채우는 반투명 진하기 — 변형 목표: 0.6 → 0.85
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var wrap = document.getElementById('wrap');
var card = document.getElementById('card');
var openBtn = document.getElementById('openBtn');
var simToggle = document.getElementById('simToggle');
var modeLabel = document.getElementById('modeLabel');
var isDemo = true;
card.style.setProperty('--fallback-alpha', FALLBACK_BG_ALPHA);
function human() { if (isDemo) { wrap.classList.remove('is-demo'); isDemo = false; } }
var open = false;
function setOpen(on) {
open = on;
card.classList.toggle('is-open', on);
openBtn.setAttribute('aria-pressed', String(on));
}
openBtn.addEventListener('click', function () { human(); setOpen(!open); });
var simOn = false;
function setSim(on) {
simOn = on;
wrap.classList.toggle('force-fallback', on);
simToggle.setAttribute('aria-pressed', String(on));
modeLabel.textContent = on ? 'ぼかしフォールバック版' : 'ぼかし対応版';
}
simToggle.addEventListener('click', function () { human(); setSim(!simOn); });
var last = 0;
var STEP_MS = 620;
var phase = 0;
function loop(ts) {
if (!last) last = ts;
if (isDemo && !reduce && ts - last >= STEP_MS) {
last = ts;
phase = (phase + 1) % 4;
setOpen(phase === 1 || phase === 2);
if (phase === 0) setSim(!simOn);
}
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
})();// BackdropFilterPrefix — vanilla 판(run/375/demo/02-compat-backdrop-filter-prefix/index.html) 의 React 포트.
// 폴백 배경(반투명 단색)을 항상 선언하고 @supports(backdrop-filter) 가 지원판에서 흐림으로 덮는다.
import { useEffect, useRef } from "react";
import styles from "./BackdropFilterPrefix.module.scss";
type Box<T> = { current: T };
const FALLBACK_BG_ALPHA = 0.6; // 흐림이 없는 환경에서 카드 배경을 채우는 반투명 진하기 — 변형 목표: 0.6 → 0.85
const STEP_MS = 620;
export default function BackdropFilterPrefix() {
const rootRef = useRef(null) as unknown as Box<HTMLDivElement>;
const cardRef = useRef(null) as unknown as Box<HTMLDivElement>;
const openBtnRef = 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 card = cardRef.current;
const openBtn = openBtnRef.current;
const simToggle = simToggleRef.current;
const modeLabel = modeLabelRef.current;
if (!wrap || !card || !openBtn || !simToggle || !modeLabel) return;
let isDemo = true;
card.style.setProperty("--fallback-alpha", String(FALLBACK_BG_ALPHA));
function human() { if (isDemo) { wrap!.classList.remove(styles.isDemo); isDemo = false; } }
let open = false;
function setOpen(on: boolean) {
open = on;
card!.classList.toggle(styles.isOpen, on);
openBtn!.setAttribute("aria-pressed", String(on));
}
function onOpenClick() { human(); setOpen(!open); }
openBtn.addEventListener("click", onOpenClick);
let simOn = false;
function setSim(on: boolean) {
simOn = on;
wrap!.classList.toggle(styles.forceFallback, on);
simToggle!.setAttribute("aria-pressed", String(on));
modeLabel!.textContent = on ? "흐림 폴백판" : "흐림 지원판";
}
function onSimClick() { human(); setSim(!simOn); }
simToggle.addEventListener("click", onSimClick);
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) % 4;
setOpen(phase === 1 || phase === 2);
if (phase === 0) setSim(!simOn);
}
rafRef.current = requestAnimationFrame(loop);
}
rafRef.current = requestAnimationFrame(loop);
return () => {
openBtn!.removeEventListener("click", onOpenClick);
simToggle!.removeEventListener("click", onSimClick);
cancelAnimationFrame(rafRef.current);
};
}, []);
return (
<main className={styles.stage}>
<div className={styles.wrap + " " + styles.isDemo} ref={rootRef}>
<div className={styles.backdrop}></div>
<button className={styles.openBtn} ref={openBtnRef} type="button" aria-pressed="false">카드 열기</button>
<div className={styles.card} ref={cardRef}>
<p className={styles.cardText}>알림 3건</p>
</div>
<div className={styles.foot}>
<span className={styles.modeLabel} ref={modeLabelRef}>흐림 지원판</span>
<button className={styles.simToggle} ref={simToggleRef} type="button" aria-pressed="false">폴백 흉내내기</button>
</div>
</div>
</main>
);
}02 背景ぼかしフォールバック — ぼかしが効かない環境で半透明背景に置き換わるカード — 計測 累積面積 7.263% · 動いたフレーム 8/23GODRICH オリジナル · SCSS と React は zip に同梱 · css ブラウザ対応 9選 — 実務で使うフォールバック集
このデモが属する記事
css ブラウザ対応 9選 — 実務で使うフォールバック集9つのcss ブラウザ対応トリックのうち、1つはアドレスバーの開閉のたびに画面全体の高さを再計算し、1つは入力欄にフォーカスしても画面が拡大されないようロックし、1つはリストを端まで引っ張っても背景が一緒に揺れないよう止めます。 この記事のデモをすべて見る


