09Vào màn hình bằng @starting-style
@starting-style cùng transition-behavior: allow-discrete tạo ngay được chuyển cảnh từ display: none sang hiển thị mà không mất một keyframe nào, còn trong demo một đoạn JS ngắn phát lại khoảnh khắc đó theo chu kỳ.
Dùng choToast báo lưu xong, badge thông báo mới vừa hiện trên hero
@starting-styletransition-behavior: allow-discretedisplay transition

<main class="stage">
<div class="scene">
<p class="scene__label">Auto-save</p>
<div class="toast" id="toast">Saved</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-quick; // 220ms — @starting-style 진입 전환 길이 (반복 재생은 JS 가 담당, index.html 참고)
$easing: $ease-pop;
$color: #9fbed0;
@include stage($stage-ink);
.scene {
box-sizing: border-box;
width: clamp(220px, 62vmin, 300px);
min-height: 160px;
display: grid;
place-items: center;
gap: $sp-4;
}
.scene__label {
margin: 0;
color: rgba($subject-cream, .75);
font-weight: 700;
font-size: clamp(11px, 3vw, 12px);
line-height: 1;
font-family: inherit;
letter-spacing: .08em;
text-transform: uppercase;
}
.toast {
padding: $sp-3 $sp-6;
border-radius: $r-pill;
background: $color;
color: $stage-ink;
font-weight: 800;
font-size: clamp(13px, 3.6vw, 14px);
line-height: 1;
font-family: inherit;
box-shadow: $shadow-raised;
}
// Chrome 117+ 만 지원 — 미지원 브라우저는 이 블록 밖의 기본 상태(항상 보임, 전환 없음)로 조용히 대체된다
@supports (transition-behavior: allow-discrete) {
.toast {
transition: display $duration allow-discrete, opacity $duration $easing, transform $duration $easing;
}
.toast.is-hidden {
display: none;
opacity: 0;
transform: translateY($sp-3) scale(.92);
}
// display:none → 보임으로 "새로 렌더" 되는 그 순간의 시작 상태. 키프레임 없이 CSS 만으로 진입 전환을 만든다
@starting-style {
.toast { opacity: 0; transform: translateY($sp-3) scale(.92); }
}
}
@media (prefers-reduced-motion: reduce) {
.toast { transition: none !important; }
}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);
}
.scene {
box-sizing: border-box;
width: clamp(220px, 62vmin, 300px);
min-height: 160px;
display: grid;
place-items: center;
gap: 16px;
}
.scene__label {
margin: 0;
color: rgba(255, 247, 230, 0.75);
font-weight: 700;
font-size: clamp(11px, 3vw, 12px);
line-height: 1;
font-family: inherit;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.toast {
padding: 12px 24px;
border-radius: 999px;
background: #9fbed0;
color: #17141a;
font-weight: 800;
font-size: clamp(13px, 3.6vw, 14px);
line-height: 1;
font-family: inherit;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
}
@supports (transition-behavior: allow-discrete) {
.toast {
transition: display 220ms allow-discrete, opacity 220ms cubic-bezier(0.2, 1.4, 0.4, 1), transform 220ms cubic-bezier(0.2, 1.4, 0.4, 1);
}
.toast.is-hidden {
display: none;
opacity: 0;
transform: translateY(12px) scale(0.92);
}
@starting-style {
.toast {
opacity: 0;
transform: translateY(12px) scale(0.92);
}
}
}
@media (prefers-reduced-motion: reduce) {
.toast {
transition: none !important;
}
}// @starting-style 는 요소가 "새로 렌더" 될 때만 발동한다. 데모에서 반복 재현하려면
// display:none 으로 뺐다가(is-hidden) 두 번의 requestAnimationFrame 뒤에 다시 넣어
// 매번 새로 렌더된 것처럼 만든다 — 8줄.
(function () {
var t = document.getElementById('toast');
function loop() {
t.classList.add('is-hidden');
requestAnimationFrame(function () { requestAnimationFrame(function () { t.classList.remove('is-hidden'); }); });
}
setTimeout(function () { loop(); setInterval(loop, 350); }, 300);
})();import { useEffect, useState } from "react";
import type { CSSProperties } from "react";
import styles from "./StartingStyleLoad.module.scss";
export type StartingStyleLoadProps = {
label?: string;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function StartingStyleLoad({
label = "Saved",
duration = 220,
easing = "cubic-bezier(.2,1.4,.4,1)",
color = "#38b6ff",
}: StartingStyleLoadProps) {
const [hidden, setHidden] = useState(false);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
// @starting-style 는 요소가 "새로 렌더" 될 때만 발동한다 — 실서비스라면 실제 저장 이벤트가
// 이 토글을 부르고, 여기서는 데모용으로 짧은 간격을 두고 반복 재현한다
useEffect(() => {
const id = setInterval(() => {
setHidden(true);
requestAnimationFrame(() => requestAnimationFrame(() => setHidden(false)));
}, 350);
return () => clearInterval(id);
}, []);
return (
<div className={styles.scene} style={vars}>
<p className={styles.label}>Auto-save</p>
<div className={`${styles.toast} ${hidden ? styles.hidden : ""}`}>{label}</div>
</div>
);
}09 Vào màn hình bằng @starting-style — Diện tích đo được 2.515% · khung hình chuyển động 8/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · 9 hero section css animation — không cần JS
Bài viết chứa demo này
9 hero section css animation — không cần JSHero section là màn hình lớn đập vào mắt đầu tiên khi bạn vừa vào một trang web, nơi đặt tiêu đề cỡ đại và nút bấm dựng Xem tất cả demo trong bài này


