05이미지 블러업
아주 작은 이미지를 흐리게 보여 주다가 진짜 이미지로 선명해진다.
쓰임히어로 이미지, 갤러리
20px400ms확대 섞기

<main class="stage">
<div class="photo">
<div class="placeholder"></div>
<div class="real"></div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-loop; // 흐림 → 선명 → 흐림 한 바퀴
$easing: linear; // 스프링·팝은 중간에서 멈춘 듯 보였다 — 고르게 바뀌어야 눈에 계속 보인다
$color: $subject-blue; // 진짜 이미지 쪽의 강조 색
@include stage($stage-ink);
// 진짜 이미지 대신 CSS 그라디언트로 사진 자리를 흉내 낸다 (외부 이미지 의존 0)
.photo {
position: relative;
overflow: hidden;
width: clamp(200px, 70%, 320px);
aspect-ratio: 4 / 3;
border-radius: $r-card;
box-shadow: $shadow-raised;
}
// 결이 있는 무늬라야 "흐림"이 실제로 다르게 보인다 (매끈한 그라디언트는 흐려도 거의 같은 색이다)
.placeholder, .real {
position: absolute;
inset: 0;
background-image:
repeating-linear-gradient(135deg, rgba($subject-cream, .85) 0 6px, rgba($color, .85) 6px 12px),
radial-gradient(circle at 30% 30%, rgba($subject-cream, .5), transparent 55%);
background-color: $stage-ink;
}
// 아주 작은 원본을 흐리게 미리 깐다 (20px, 고정값 — 이 레이어는 움직이지 않는다). 결이 뭉개져 평평한 색 덩어리가 된다
.placeholder {
filter: blur(20px);
transform: scale(1.15); // 블러 가장자리가 비치지 않게 살짝 키운다
}
// 진짜 이미지가 흐림 위로 나타났다 사라지길 반복한다 (렌더용 루프)
.real {
opacity: 0;
transform: scale(1.04);
animation: reveal $duration $easing infinite;
}
@keyframes reveal {
0% { opacity: 0; transform: scale(1.04); } // 흐림만 보인다
50% { opacity: 1; transform: scale(1); } // 가장 선명한 순간
100% { opacity: 0; transform: scale(1.04); } // 다시 흐림으로 — 한 바퀴가 곧 로딩 한 번
}
// ── 접근성: 흐림-선명 반복 대신 선명한 최종 상태만 고정으로 보여준다 ──
@media (prefers-reduced-motion: reduce) {
.real { animation: none !important; opacity: 1; transform: scale(1); }
}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);
}
.photo {
position: relative;
overflow: hidden;
width: clamp(200px, 70%, 320px);
aspect-ratio: 4/3;
border-radius: 16px;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
}
.placeholder, .real {
position: absolute;
inset: 0;
background-image: repeating-linear-gradient(135deg, rgba(255, 247, 230, 0.85) 0 6px, rgba(47, 109, 246, 0.85) 6px 12px), radial-gradient(circle at 30% 30%, rgba(255, 247, 230, 0.5), transparent 55%);
background-color: #17141a;
}
.placeholder {
filter: blur(20px);
transform: scale(1.15);
}
.real {
opacity: 0;
transform: scale(1.04);
animation: reveal 2s linear infinite;
}
@keyframes reveal {
0% {
opacity: 0;
transform: scale(1.04);
}
50% {
opacity: 1;
transform: scale(1);
}
100% {
opacity: 0;
transform: scale(1.04);
}
}
@media (prefers-reduced-motion: reduce) {
.real {
animation: none !important;
opacity: 1;
transform: scale(1);
}
}import { useCallback, useState } from "react";
import type { CSSProperties } from "react";
import styles from "./ImageBlurUp.module.scss";
export type ImageBlurUpProps = {
/** 진짜 이미지 */
src: string;
/** 아주 작은(20px 안팎) 미리 준비한 흐린 이미지 */
placeholderSrc: string;
alt: string;
/** 흐림 → 선명 전환 시간 (기본 400ms) */
duration?: number;
easing?: string;
className?: string;
};
export default function ImageBlurUp({
src,
placeholderSrc,
alt,
duration = 400,
easing = "cubic-bezier(.2,.8,.2,1)",
className = "",
}: ImageBlurUpProps) {
const [loaded, setLoaded] = useState(false);
const onLoad = useCallback(() => setLoaded(true), []);
const vars = { "--duration": `${duration}ms`, "--easing": easing } as CSSProperties;
return (
<div className={`${styles.photo} ${className}`} style={vars}>
<img className={styles.placeholder} src={placeholderSrc} alt="" aria-hidden="true" />
<img
className={`${styles.real} ${loaded ? styles.loaded : ""}`}
src={src}
alt={alt}
onLoad={onLoad}
/>
</div>
);
}05 이미지 블러업 — 측정 누적면적 49.817% · 움직인 프레임 22/23GODRICH 원작 · zip 에 SCSS·React 포함 · 스켈레톤 UI CSS 9종 — 복붙하면 되는 로딩 화면
이 데모가 속한 글
스켈레톤 UI CSS 9종 — 복붙하면 되는 로딩 화면스켈레톤 UI CSS는 실제 글자·이미지가 도착하기 전에 그 자리를 회색 막대나 동그라미로 미리 그려 두는 로딩 방식입니다. 빈 화면이나 뱅글뱅글 도는 스피너보다 "곧 뭔가 나온다"는 확신을 먼저 줍니다. 아래 아홉 이 글의 데모 전부 보기


