01ふわっと表示
画面に入ると下からふわっと現れる。
使いどころ見出し・カード・段落など
24px멀리 48px페이드만

<main class="stage">
<article class="card is-demo">
<p class="card__eyebrow">SECTION</p>
<h2 class="card__title">ふわっと浮かび上がる</h2>
<p class="card__body">スクロールして表示されると、下からふわっと浮かび上がって現れます。</p>
</article>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개. 변형은 이것만 바꾼다 (값은 토큰에서 받는다) ──────
$distance: 24px; // 올라오는 거리. 멀리 두려면 48px
$duration: $dur-enter; // 등장이 다 끝나는 데 걸리는 진행 구간
$easing: $ease-out; // 처음 빠르고 끝은 느리게
// ── 데모 무대 (zip 에서 지워도 된다) ──────────────────────────
@include stage($stage-ink);
// ── 카드 (피사체: ink 무대라 blue) ───────────────────────────
.card {
box-sizing: border-box;
width: min(300px, 100%);
padding: clamp(16px, 6vw, 26px) clamp(16px, 6vw, 24px);
border-radius: $r-card;
background: $subject-blue-deep;
color: #fff;
text-align: left;
&__eyebrow { margin: 0 0 6px; font-weight: 700; font-size: 11px; line-height: 1; font-family: inherit; letter-spacing: .12em; color: $subject-cream; }
&__title { margin: 0 0 8px; font-weight: 700; font-size: clamp(16px, 5vw, 20px); line-height: 1.3; font-family: inherit; }
&__body { margin: 0; font-weight: 400; font-size: 14px; line-height: 1.5; font-family: inherit; color: $subject-cream; }
// ── 실제 사용 코드: 스크롤이 곧 애니메이션 시계다 (zip 그대로) ──
// 요소가 뷰포트로 들어오는 구간(entry)만큼 진행된다. JS 0줄.
@supports (animation-timeline: view()) {
animation: fade-up-reveal $easing both;
animation-timeline: view();
animation-range: entry 0% cover 30%;
}
// ── 미지원 브라우저: 움직이지 않고 그냥 보여준다 (정직한 대체) ──
@supports not (animation-timeline: view()) {
opacity: 1;
transform: none;
}
}
@keyframes fade-up-reveal {
from { opacity: 0; transform: translateY($distance); }
to { opacity: 1; transform: translateY(0); }
}
// ── 데모(.is-demo): 렌더러가 프레임을 뽑도록 스크롤 대신 2초 루프로 흉내 ──
.card.is-demo {
animation: fade-up-loop $dur-loop $easing infinite;
}
@keyframes fade-up-loop {
0% { opacity: 0; transform: translateY($distance * 2); }
30% { opacity: 1; transform: translateY(0); }
75% { opacity: 1; transform: translateY(0); }
100% { opacity: 0; transform: translateY($distance * 2); }
}
// ── 접근성: 움직임 줄이기 설정이면 즉시 보여주기만 ────────────
@media (prefers-reduced-motion: reduce) {
.card, .card.is-demo { animation: none !important; opacity: 1; transform: 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);
}
.card {
box-sizing: border-box;
width: min(300px, 100%);
padding: clamp(16px, 6vw, 26px) clamp(16px, 6vw, 24px);
border-radius: 16px;
background: #0a4fe8;
color: #fff;
text-align: left;
}
.card__eyebrow {
margin: 0 0 6px;
font-weight: 700;
font-size: 11px;
line-height: 1;
font-family: inherit;
letter-spacing: 0.12em;
color: #fff7e6;
}
.card__title {
margin: 0 0 8px;
font-weight: 700;
font-size: clamp(16px, 5vw, 20px);
line-height: 1.3;
font-family: inherit;
}
.card__body {
margin: 0;
font-weight: 400;
font-size: 14px;
line-height: 1.5;
font-family: inherit;
color: #fff7e6;
}
@supports (animation-timeline: view()) {
.card {
animation: fade-up-reveal ease-out both;
animation-timeline: view();
animation-range: entry 0% cover 30%;
}
}
@supports not (animation-timeline: view()) {
.card {
opacity: 1;
transform: none;
}
}
@keyframes fade-up-reveal {
from {
opacity: 0;
transform: translateY(24px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card.is-demo {
animation: fade-up-loop 2s ease-out infinite;
}
@keyframes fade-up-loop {
0% {
opacity: 0;
transform: translateY(48px);
}
30% {
opacity: 1;
transform: translateY(0);
}
75% {
opacity: 1;
transform: translateY(0);
}
100% {
opacity: 0;
transform: translateY(48px);
}
}
@media (prefers-reduced-motion: reduce) {
.card, .card.is-demo {
animation: none !important;
opacity: 1;
transform: none;
}
}import type { CSSProperties, ReactNode } from "react";
import styles from "./FadeUpReveal.module.scss";
export type FadeUpRevealProps = {
eyebrow?: string;
title: string;
children: ReactNode;
/** 올라오는 거리 (기본 24px) */
distance?: number;
/** 등장이 끝나는 구간 (기본 700ms) — animation-range 계산에는 안 쓰이고 문서용 */
duration?: number;
/** easing (기본 ease-out) */
easing?: string;
className?: string;
};
type Vars = CSSProperties & {
"--distance"?: string;
"--duration"?: string;
"--easing"?: string;
};
export default function FadeUpReveal({
eyebrow = "SECTION",
title,
children,
distance = 24,
duration = 700,
easing = "ease-out",
className = "",
}: FadeUpRevealProps) {
const vars: Vars = {
"--distance": `${distance}px`,
"--duration": `${duration}ms`,
"--easing": easing,
};
return (
<article className={`${styles.card} ${className}`} style={vars}>
<p className={styles.eyebrow}>{eyebrow}</p>
<h2 className={styles.title}>{title}</h2>
<p className={styles.body}>{children}</p>
</article>
);
}01 ふわっと表示 — 計測 累積面積 38.606% · 動いたフレーム 13/23GODRICH オリジナル · SCSS と React は zip に同梱 · 【コピペOK】スクロール アニメーション CSS のみ 9選
このデモが属する記事
【コピペOK】スクロール アニメーション CSS のみ 9選スクロール アニメーション css は、要素が画面に入った量やページ全体の読了率を animation-timeline に渡し、JavaScript を1行も書かずに動かす仕組みです。この記事では9種類を選び、view() と scrol この記事のデモをすべて見る


