08궤도 오브 푸터
푸터 상단 구분선을 따라 미니 오브 세 개가 서로 다른 속도로 좌우로 지나가고(animation-delay), 링크에 마우스를 올리면 글자가 밝아집니다. 네 열은 좁은 화면에서 한 열로 접힙니다.
쓰임멀티 컬럼 푸터, 사이트 마무리
translateXanimation-delaygrid-template-columns

<main class="stage">
<footer class="fo">
<!-- 구분선 위를 지나가는 미니 오브 셋 — 장식이라 낭독기에서 숨긴다 -->
<div class="fo__rail" aria-hidden="true">
<span class="fo__beacon fo__beacon--slow"><span class="fo__dot fo__dot--lilac"></span></span>
<span class="fo__beacon fo__beacon--fast"><span class="fo__dot fo__dot--mint"></span></span>
<span class="fo__beacon fo__beacon--drift"><span class="fo__dot fo__dot--blue"></span></span>
</div>
<div class="fo__cols">
<section class="fo__col">
<h2 class="fo__head">제품</h2>
<ul class="fo__links">
<li><a href="#">기능</a></li>
<li><a href="#">요금제</a></li>
<li><a href="#">변경 기록</a></li>
</ul>
</section>
<section class="fo__col">
<h2 class="fo__head">문서</h2>
<ul class="fo__links">
<li><a href="#">빠른 시작</a></li>
<li><a href="#">API</a></li>
<li><a href="#">예제</a></li>
</ul>
</section>
<section class="fo__col">
<h2 class="fo__head">회사</h2>
<ul class="fo__links">
<li><a href="#">소개</a></li>
<li><a href="#">채용</a></li>
<li><a href="#">문의</a></li>
</ul>
</section>
<section class="fo__col">
<h2 class="fo__head">커뮤니티</h2>
<ul class="fo__links">
<li><a href="#">디스코드</a></li>
<li><a href="#">블로그</a></li>
<li><a href="#">상태</a></li>
</ul>
</section>
</div>
<p class="fo__copy">
<span>© 2026 Orbit Labs</span>
<span class="fo__lang">
<span class="fo__ico" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M213.09,172.48a96,96,0,0,1-80.41,51.41l3.17-16.44a8,8,0,0,0-2-6.95l-19.74-20.33a8,8,0,0,1-1.44-8.69l13.7-30.74a8,8,0,0,1,8.38-4.67l22.82,3.08a8.11,8.11,0,0,1,3.12,1.11ZM116.71,95,129,88.24a7.46,7.46,0,0,0,1.5-1.07l26.91-24.33A8,8,0,0,0,159,53l-10.5-18.81A96.62,96.62,0,0,0,128,32,95.61,95.61,0,0,0,67.78,53.23L56,81.08A8,8,0,0,0,55.88,87l11.5,30.67a8,8,0,0,0,5.81,5l2.69.58L89.2,100a8,8,0,0,1,6.94-4h16.71A7.9,7.9,0,0,0,116.71,95Z" opacity="0.2"/><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm88,104a87.62,87.62,0,0,1-6.4,32.94l-44.7-27.49a15.92,15.92,0,0,0-6.24-2.23l-22.82-3.08a16.11,16.11,0,0,0-16,7.86h-8.72l-3.8-7.86a15.91,15.91,0,0,0-11-8.67l-8-1.73L96.14,104h16.71a16.06,16.06,0,0,0,7.73-2l12.25-6.76a16.62,16.62,0,0,0,3-2.14l26.91-24.34A15.93,15.93,0,0,0,166,49.1l-.36-.65A88.11,88.11,0,0,1,216,128ZM143.31,41.34,152,56.9,125.09,81.24,112.85,88H96.14a16,16,0,0,0-13.88,8l-8.73,15.23L63.38,84.19,74.32,58.32a87.87,87.87,0,0,1,69-17ZM40,128a87.53,87.53,0,0,1,8.54-37.8l11.34,30.27a16,16,0,0,0,11.62,10l21.43,4.61L96.74,143a16.09,16.09,0,0,0,14.4,9h1.48l-7.23,16.23a16,16,0,0,0,2.86,17.37l.14.14L128,205.94l-1.94,10A88.11,88.11,0,0,1,40,128Zm102.58,86.78,1.13-5.81a16.09,16.09,0,0,0-4-13.9,1.85,1.85,0,0,1-.14-.14L120,174.74,133.7,144l22.82,3.08,45.72,28.12A88.18,88.18,0,0,1,142.58,214.78Z"/></svg></span>
한국어
</span>
</p>
</footer>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 느린 오브가 구분선을 한 번 건너는 시간 (빠른 쪽은 이것의 절반)
$easing: linear; // 지나가는 빛은 가속하지 않는다
$color: $subject-lilac; // 첫 오브 색
@include stage($stage-paper);
.fo {
position: relative; overflow: hidden; // 오브가 푸터 밖으로 새지 않게
box-sizing: border-box; width: min(100%, 440px);
background: $subject-ink; border-radius: $r-card; box-shadow: $shadow-raised;
padding: $sp-4 $sp-5 $sp-4;
color: $subject-cream;
}
// ── 상단 구분선 = 오브가 지나가는 궤도
.fo__rail {
position: relative; height: 2px; border-radius: $r-pill;
background: rgba($subject-cream, .18);
}
// 캐리어. 폭이 궤도와 같아서 translateX 의 % 가 곧 "궤도의 몇 %" 가 된다
// (점 자체에 % 를 주면 20·14·24px 짜리 점 자기 폭의 % 라 궤도를 못 건넌다)
.fo__beacon {
position: absolute; left: 0; top: 50%; width: 100%; height: 0;
animation-name: beaconCross;
animation-timing-function: $easing;
animation-iteration-count: infinite;
animation-fill-mode: backwards; // 지연이 걸린 루프 — 없으면 첫 프레임에 기본 상태가 번쩍인다
}
.fo__beacon--slow { animation-duration: $duration; animation-delay: -400ms; }
.fo__beacon--fast { animation-duration: $dur-loop * .5; animation-delay: -500ms; }
.fo__beacon--drift { animation-duration: $duration; animation-delay: -1200ms; }
// 음수 지연으로 셋의 위상을 20% · 50% · 62% 로 벌려 둔다 — 어느 순간에도 최소 하나는 궤도 안에 있다
// (poster 가 빈 구분선이 되지 않게)
.fo__dot {
position: absolute; left: 0; top: 0; transform: translate(-50%, -50%);
width: 20px; height: 20px; border-radius: 50%;
}
.fo__dot--lilac { background: radial-gradient(circle at 50% 50%, rgba($color, .92) 0%, rgba($color, .34) 42%, rgba($color, 0) 72%); }
.fo__dot--mint { width: 14px; height: 14px;
background: radial-gradient(circle at 50% 50%, rgba($subject-mint, .92) 0%, rgba($subject-mint, .34) 42%, rgba($subject-mint, 0) 72%); }
.fo__dot--blue { width: 24px; height: 24px;
background: radial-gradient(circle at 50% 50%, rgba($subject-sky, .88) 0%, rgba($subject-blue, .34) 42%, rgba($subject-blue, 0) 72%); }
@keyframes beaconCross {
from { transform: translateX(-8%); }
to { transform: translateX(108%); }
}
// ── 네 열
.fo__cols {
display: grid; grid-template-columns: repeat(4, 1fr); gap: $sp-4 $sp-3;
margin-top: $sp-4;
}
.fo__col { min-width: 0; }
.fo__head {
margin: 0; font-weight: 800; font-size: clamp(9px, 2.6vw, 10px); line-height: 1;
letter-spacing: .03em; color: $subject-cream; // 17.11:1
}
.fo__links { list-style: none; margin: $sp-2 0 0; padding: 0; }
.fo__links li { margin-top: $sp-1; }
.fo__links li:first-child { margin-top: 0; }
.fo__links a {
font-weight: 600; font-size: clamp(8px, 2.4vw, 10px); line-height: 1.3;
color: rgba($subject-cream, .72); // 9.18:1
text-decoration: none; transition: color $dur-quick $ease-out;
}
.fo__links a:hover,
.fo__links a:focus-visible { color: $subject-cream; }
.fo__copy {
display: flex; align-items: center; justify-content: space-between; gap: $sp-2;
margin: $sp-4 0 0; padding-top: $sp-3;
border-top: 1px solid rgba($subject-cream, .14);
font-weight: 600; font-size: clamp(7px, 2.2vw, 9px); line-height: 1;
color: rgba($subject-cream, .62); // 7.12:1
}
.fo__lang { display: inline-flex; align-items: center; gap: $sp-1; }
.fo__ico { display: inline-flex; width: 12px; height: 12px; }
.fo__ico svg { width: 100%; height: 100%; }
// ── 320×200 폰 실측 대비 압축 (BD9). 네 열이 한 열로 접힌다 — 이 블록 안에서는 간격을 올리지 않는다
@media (max-width: 340px), (max-height: 220px) {
.fo { padding: $sp-3; }
.fo__cols { grid-template-columns: 1fr; gap: $sp-2; margin-top: $sp-3; }
// 한 열이 되면 제목과 링크를 한 줄에 눕혀 네 덩어리가 폰 높이 안에 들어오게 한다
.fo__col { display: flex; align-items: baseline; gap: $sp-2; }
.fo__head { font-size: 8px; flex: 0 0 auto; }
.fo__links { display: flex; flex-wrap: wrap; gap: $sp-2; margin-top: 0; }
.fo__links li { margin-top: 0; }
.fo__links a { font-size: 8px; }
.fo__dot { width: 14px; height: 14px; }
.fo__dot--mint { width: 10px; height: 10px; }
.fo__dot--blue { width: 16px; height: 16px; }
.fo__copy { margin-top: $sp-2; padding-top: $sp-2; font-size: 7px; }
.fo__ico { width: 10px; height: 10px; }
}
@media (prefers-reduced-motion: reduce) {
.fo__beacon { animation: none !important; }
.fo__beacon--slow { transform: translateX(20%) !important; }
.fo__beacon--fast { transform: translateX(50%) !important; }
.fo__beacon--drift { transform: translateX(62%) !important; }
.fo__links a { 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: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.fo {
position: relative;
overflow: hidden;
box-sizing: border-box;
width: min(100%, 440px);
background: #17141a;
border-radius: 16px;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
padding: 16px 20px 16px;
color: #fff7e6;
}
.fo__rail {
position: relative;
height: 2px;
border-radius: 999px;
background: rgba(255, 247, 230, 0.18);
}
.fo__beacon {
position: absolute;
left: 0;
top: 50%;
width: 100%;
height: 0;
animation-name: beaconCross;
animation-timing-function: linear;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.fo__beacon--slow {
animation-duration: 2s;
animation-delay: -400ms;
}
.fo__beacon--fast {
animation-duration: 1s;
animation-delay: -500ms;
}
.fo__beacon--drift {
animation-duration: 2s;
animation-delay: -1200ms;
}
.fo__dot {
position: absolute;
left: 0;
top: 0;
transform: translate(-50%, -50%);
width: 20px;
height: 20px;
border-radius: 50%;
}
.fo__dot--lilac {
background: radial-gradient(circle at 50% 50%, rgba(185, 160, 255, 0.92) 0%, rgba(185, 160, 255, 0.34) 42%, rgba(185, 160, 255, 0) 72%);
}
.fo__dot--mint {
width: 14px;
height: 14px;
background: radial-gradient(circle at 50% 50%, rgba(76, 212, 166, 0.92) 0%, rgba(76, 212, 166, 0.34) 42%, rgba(76, 212, 166, 0) 72%);
}
.fo__dot--blue {
width: 24px;
height: 24px;
background: radial-gradient(circle at 50% 50%, rgba(110, 200, 255, 0.88) 0%, rgba(47, 109, 246, 0.34) 42%, rgba(47, 109, 246, 0) 72%);
}
@keyframes beaconCross {
from {
transform: translateX(-8%);
}
to {
transform: translateX(108%);
}
}
.fo__cols {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px 12px;
margin-top: 16px;
}
.fo__col {
min-width: 0;
}
.fo__head {
margin: 0;
font-weight: 800;
font-size: clamp(9px, 2.6vw, 10px);
line-height: 1;
letter-spacing: 0.03em;
color: #fff7e6;
}
.fo__links {
list-style: none;
margin: 8px 0 0;
padding: 0;
}
.fo__links li {
margin-top: 4px;
}
.fo__links li:first-child {
margin-top: 0;
}
.fo__links a {
font-weight: 600;
font-size: clamp(8px, 2.4vw, 10px);
line-height: 1.3;
color: rgba(255, 247, 230, 0.72);
text-decoration: none;
transition: color 220ms ease-out;
}
.fo__links a:hover,
.fo__links a:focus-visible {
color: #fff7e6;
}
.fo__copy {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin: 16px 0 0;
padding-top: 12px;
border-top: 1px solid rgba(255, 247, 230, 0.14);
font-weight: 600;
font-size: clamp(7px, 2.2vw, 9px);
line-height: 1;
color: rgba(255, 247, 230, 0.62);
}
.fo__lang {
display: inline-flex;
align-items: center;
gap: 4px;
}
.fo__ico {
display: inline-flex;
width: 12px;
height: 12px;
}
.fo__ico svg {
width: 100%;
height: 100%;
}
@media (max-width: 340px), (max-height: 220px) {
.fo {
padding: 12px;
}
.fo__cols {
grid-template-columns: 1fr;
gap: 8px;
margin-top: 12px;
}
.fo__col {
display: flex;
align-items: baseline;
gap: 8px;
}
.fo__head {
font-size: 8px;
flex: 0 0 auto;
}
.fo__links {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 0;
}
.fo__links li {
margin-top: 0;
}
.fo__links a {
font-size: 8px;
}
.fo__dot {
width: 14px;
height: 14px;
}
.fo__dot--mint {
width: 10px;
height: 10px;
}
.fo__dot--blue {
width: 16px;
height: 16px;
}
.fo__copy {
margin-top: 8px;
padding-top: 8px;
font-size: 7px;
}
.fo__ico {
width: 10px;
height: 10px;
}
}
@media (prefers-reduced-motion: reduce) {
.fo__beacon {
animation: none !important;
}
.fo__beacon--slow {
transform: translateX(20%) !important;
}
.fo__beacon--fast {
transform: translateX(50%) !important;
}
.fo__beacon--drift {
transform: translateX(62%) !important;
}
.fo__links a {
transition: none !important;
}
}import styles from "./FooterOrb.module.scss";
export type FooterOrbProps = {
/** 느린 오브가 구분선을 한 번 건너는 시간 (빠른 쪽은 이것의 절반) */
duration?: string;
/** 지나가는 빛의 이징 — 가속하지 않는다 */
easing?: string;
/** 첫 오브 색 — --beaconColor 로 내려간다 */
color?: string;
};
type Column = { head: string; links: string[] };
const COLUMNS: Column[] = [
{ head: "제품", links: ["기능", "요금제", "변경 기록"] },
{ head: "문서", links: ["빠른 시작", "API", "예제"] },
{ head: "회사", links: ["소개", "채용", "문의"] },
{ head: "커뮤니티", links: ["디스코드", "블로그", "상태"] },
];
// 음수 지연으로 셋의 위상을 20% · 50% · 62% 로 벌려 둔다 — 어느 순간에도 최소 하나는 궤도 안에 있다
const BEACONS = [
{ dot: styles.dotLilac, delay: "-400ms", half: false },
{ dot: styles.dotMint, delay: "-500ms", half: true },
{ dot: styles.dotBlue, delay: "-1200ms", half: false },
];
function halfOf(duration: string): string {
const n = parseFloat(duration);
return duration.indexOf("ms") >= 0 ? `${n / 2}ms` : `${n / 2}s`;
}
export default function FooterOrb({
duration = "2s",
easing = "linear",
color = "#b9a0ff",
}: FooterOrbProps) {
return (
<main className={styles.stage}>
<footer className={styles.fo} style={{ "--beaconColor": color }}>
{/* 구분선 위를 지나가는 미니 오브 셋 — 장식이라 낭독기에서 숨긴다 */}
<div className={styles.rail} aria-hidden="true">
{BEACONS.map((b) => (
<span
key={b.delay}
className={styles.beacon}
style={{
animationName: "beaconCross",
animationDuration: b.half ? halfOf(duration) : duration,
animationTimingFunction: easing,
animationIterationCount: "infinite",
animationFillMode: "backwards",
animationDelay: b.delay,
}}
>
<span className={`${styles.dot} ${b.dot}`} />
</span>
))}
</div>
<div className={styles.cols}>
{COLUMNS.map((c) => (
<section key={c.head} className={styles.col}>
<h2 className={styles.head}>{c.head}</h2>
<ul className={styles.links}>
{c.links.map((l) => (
<li key={l}><a href="#" onClick={(e: { preventDefault: () => void }) => { e.preventDefault(); }}>{l}</a></li>
))}
</ul>
</section>
))}
</div>
<p className={styles.copy}>
<span>© 2026 Orbit Labs</span>
<span className={styles.lang}>
<span className={styles.ico} aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M213.09,172.48a96,96,0,0,1-80.41,51.41l3.17-16.44a8,8,0,0,0-2-6.95l-19.74-20.33a8,8,0,0,1-1.44-8.69l13.7-30.74a8,8,0,0,1,8.38-4.67l22.82,3.08a8.11,8.11,0,0,1,3.12,1.11ZM116.71,95,129,88.24a7.46,7.46,0,0,0,1.5-1.07l26.91-24.33A8,8,0,0,0,159,53l-10.5-18.81A96.62,96.62,0,0,0,128,32,95.61,95.61,0,0,0,67.78,53.23L56,81.08A8,8,0,0,0,55.88,87l11.5,30.67a8,8,0,0,0,5.81,5l2.69.58L89.2,100a8,8,0,0,1,6.94-4h16.71A7.9,7.9,0,0,0,116.71,95Z" opacity="0.2"/><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm88,104a87.62,87.62,0,0,1-6.4,32.94l-44.7-27.49a15.92,15.92,0,0,0-6.24-2.23l-22.82-3.08a16.11,16.11,0,0,0-16,7.86h-8.72l-3.8-7.86a15.91,15.91,0,0,0-11-8.67l-8-1.73L96.14,104h16.71a16.06,16.06,0,0,0,7.73-2l12.25-6.76a16.62,16.62,0,0,0,3-2.14l26.91-24.34A15.93,15.93,0,0,0,166,49.1l-.36-.65A88.11,88.11,0,0,1,216,128ZM143.31,41.34,152,56.9,125.09,81.24,112.85,88H96.14a16,16,0,0,0-13.88,8l-8.73,15.23L63.38,84.19,74.32,58.32a87.87,87.87,0,0,1,69-17ZM40,128a87.53,87.53,0,0,1,8.54-37.8l11.34,30.27a16,16,0,0,0,11.62,10l21.43,4.61L96.74,143a16.09,16.09,0,0,0,14.4,9h1.48l-7.23,16.23a16,16,0,0,0,2.86,17.37l.14.14L128,205.94l-1.94,10A88.11,88.11,0,0,1,40,128Zm102.58,86.78,1.13-5.81a16.09,16.09,0,0,0-4-13.9,1.85,1.85,0,0,1-.14-.14L120,174.74,133.7,144l22.82,3.08,45.72,28.12A88.18,88.18,0,0,1,142.58,214.78Z"/></svg></span>
한국어
</span>
</p>
</footer>
</main>
);
}08 궤도 오브 푸터 — 측정 누적면적 6.163% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · ai 랜딩페이지 오브 디자인 키트, 빛나는 섹션 9개
이 데모가 속한 글
ai 랜딩페이지 오브 디자인 키트, 빛나는 섹션 9개ai 랜딩페이지 오브 디자인은 화면 전체를 물들이는 대신 짙은 단색 무대 위에 빛나는 구슬 하나만 올려 제품의 성격을 말하는 방식입니다. 배경 그라디언트 없이도 AI 다워 보인다는 이 계산을 섹션 아홉 개로 잘라 정 이 글의 데모 전부 보기


