03색 반전
어두운 히어로 위에서는 투명 배경에 흰 글자였다가 히어로를 벗어나면 헤더 배경과 글자색이 통째로 뒤집힌다.
쓰임풀스크린 히어로가 있는 랜딩 페이지, 포트폴리오 상단
IntersectionObserver색 토큰 교체히어로 높이 기준

<main class="stage">
<header class="head is-demo">
<span class="head__layer head__layer--dark"></span>
<span class="head__layer head__layer--light"></span>
<span class="head__logo">GODRICH</span>
</header>
<div class="hero">
<div class="hero__sentinel"></div>
<p class="hero__title">히어로 영역</p>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-base; // 색이 뒤집히는 시간
$easing: $ease-out;
$color: $subject-ink; // 반전 뒤 헤더 배경
// ── 데모 무대 ──────────────────────────────────────────
html, body { margin: 0; overflow: hidden; }
.stage {
min-height: 100vh;
margin: 0;
background: $stage-orange;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.head {
position: sticky;
top: 0;
height: 56px;
display: flex;
align-items: center;
padding: 0 clamp(16px, 5vw, 24px);
// 두 배경 레이어를 겹쳐 두고 opacity 만 크로스페이드한다 (배경색 자체는 애니메이션하지 않는다)
&__layer { position: absolute; inset: 0; }
&__layer--dark { background: transparent; opacity: 1; transition: opacity $duration $easing; }
&__layer--light { background: $color; opacity: 0; transition: opacity $duration $easing; }
&.is-inverted &__layer--dark { opacity: 0; }
&.is-inverted &__layer--light { opacity: 1; }
// 로고도 헤더와 같이 뒤집힌다 — 주황(투명) 구간에선 잉크, 먹 구간에선 크림이라야 둘 다 AA 를 넘는다
&__logo { position: relative; color: $subject-ink; font-weight: 800; font-size: 18px; line-height: 1; font-family: inherit; letter-spacing: .04em; transition: color $duration $easing; }
&.is-inverted &__logo { color: $subject-cream; }
}
// ── 데모(.is-demo): 뒤집혔다 돌아왔다 2초 루프 ─────────────
.head.is-demo .head__layer--light {
animation: invert-loop $dur-loop $easing infinite;
}
.head.is-demo .head__logo {
animation: logo-invert $dur-loop $easing infinite;
}
@keyframes invert-loop {
0%, 15% { opacity: 0; }
45%, 70% { opacity: 1; }
95%, 100% { opacity: 0; }
}
@keyframes logo-invert {
0%, 15% { color: $subject-ink; }
45%, 70% { color: $subject-cream; }
95%, 100% { color: $subject-ink; }
}
// ── 히어로: 어두운 배경 위 흰 글자 자리 ───────────────────
.hero { position: relative; height: clamp(100px, 40vh, 220px); display: grid; place-items: center; }
.hero__sentinel { position: absolute; bottom: 0; height: 1px; width: 1px; }
.hero__title { color: $subject-ink; font-weight: 700; font-size: clamp(14px, 4vw, 18px); line-height: 1; font-family: inherit; margin: 0; }
@media (prefers-reduced-motion: reduce) {
.head__layer { transition: none; }
.head__logo { transition: none; }
.head.is-demo .head__layer--light { animation: none !important; opacity: 1; }
.head.is-demo .head__logo { animation: none !important; color: $subject-cream; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
min-height: 100vh;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.head {
position: sticky;
top: 0;
height: 56px;
display: flex;
align-items: center;
padding: 0 clamp(16px, 5vw, 24px);
}
.head__layer {
position: absolute;
inset: 0;
}
.head__layer--dark {
background: transparent;
opacity: 1;
transition: opacity 300ms ease-out;
}
.head__layer--light {
background: #17141a;
opacity: 0;
transition: opacity 300ms ease-out;
}
.head.is-inverted .head__layer--dark {
opacity: 0;
}
.head.is-inverted .head__layer--light {
opacity: 1;
}
.head__logo {
position: relative;
color: #17141a;
font-weight: 800;
font-size: 18px;
line-height: 1;
font-family: inherit;
letter-spacing: 0.04em;
transition: color 300ms ease-out;
}
.head.is-inverted .head__logo {
color: #fff7e6;
}
.head.is-demo .head__layer--light {
animation: invert-loop 2s ease-out infinite;
}
.head.is-demo .head__logo {
animation: logo-invert 2s ease-out infinite;
}
@keyframes invert-loop {
0%, 15% {
opacity: 0;
}
45%, 70% {
opacity: 1;
}
95%, 100% {
opacity: 0;
}
}
@keyframes logo-invert {
0%, 15% {
color: #17141a;
}
45%, 70% {
color: #fff7e6;
}
95%, 100% {
color: #17141a;
}
}
.hero {
position: relative;
height: clamp(100px, 40vh, 220px);
display: grid;
place-items: center;
}
.hero__sentinel {
position: absolute;
bottom: 0;
height: 1px;
width: 1px;
}
.hero__title {
color: #17141a;
font-weight: 700;
font-size: clamp(14px, 4vw, 18px);
line-height: 1;
font-family: inherit;
margin: 0;
}
@media (prefers-reduced-motion: reduce) {
.head__layer {
transition: none;
}
.head__logo {
transition: none;
}
.head.is-demo .head__layer--light {
animation: none !important;
opacity: 1;
}
.head.is-demo .head__logo {
animation: none !important;
color: #fff7e6;
}
}// 실제 페이지: 히어로 하단 센티넬을 지나면 색이 뒤집힌다 (IntersectionObserver, 6줄)
var head = document.querySelector('.head');
new IntersectionObserver(function (es) {
head.classList.toggle('is-inverted', !es[0].isIntersecting);
}).observe(document.querySelector('.hero__sentinel'));import { useEffect, useRef } from "react";
import type { CSSProperties } from "react";
import styles from "./ColorInvertHeader.module.scss";
export type ColorInvertHeaderProps = {
logo: string;
heroSentinelRef: { current: HTMLElement | null };
/** 색이 뒤집히는 시간 (기본 300ms) */
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function ColorInvertHeader({
logo,
heroSentinelRef,
duration = 300,
easing = "ease-out",
color = "#17141a",
}: ColorInvertHeaderProps) {
const head = useRef<HTMLElement>(null);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
useEffect(() => {
if (!heroSentinelRef.current) return;
const io = new IntersectionObserver(([e]) => {
head.current?.classList.toggle("is-inverted", !e.isIntersecting);
});
io.observe(heroSentinelRef.current);
return () => io.disconnect();
}, [heroSentinelRef]);
return (
<header ref={head} className={styles.head} style={vars}>
<span className={styles.layerDark} />
<span className={styles.layerLight} />
<span className={styles.logo}>{logo}</span>
</header>
);
}03 색 반전 — 측정 누적면적 18.667% · 움직인 프레임 13/23GODRICH 원작 · zip 에 SCSS·React 포함 · CSS 헤더 고정 스티키 9종 — AOS·GSAP 없이, JS 0줄
이 데모가 속한 글
CSS 헤더 고정 스티키 9종 — AOS·GSAP 없이, JS 0줄CSS 헤더 고정 스티키는 페이지를 스크롤해도 헤더가 화면 위에 계속 붙어 있게 만드는 방식입니다. 아홉 개 중 넷(04·05·08·09 중 04·05)은 자바스크립트 없이 CSS만으로 움직입니다. 아래 격자에서 자 이 글의 데모 전부 보기


