07フッターで停止
普段は画面に固定表示されますが、フッターが近づくと重ならないよう translateY であらかじめ少し上に位置を上げて止まります。
使いどころフッターにリンクが多いサイト、フッターが短くボタンと重なりやすいページ
IntersectionObservertranslateYJS 9줄

<main class="stage is-demo">
<div class="page-mock" aria-hidden="true">
<span class="page-mock__line"></span>
<span class="page-mock__line"></span>
</div>
<footer class="footer-mock" aria-hidden="true"></footer>
<button type="button" class="totop" aria-label="トップへ">
<svg class="totop__arrow" viewBox="0 0 24 24" aria-hidden="true"><polyline points="5,15 12,8 19,15"/></svg>
</button>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: 220ms; // 버튼이 들어 올려지는 실사용 전환 시간
$easing: $ease-out;
$color: $subject-ink; // 화살표 색
@include stage($stage-orange);
.stage { position: relative; overflow: hidden; }
.page-mock {
position: absolute;
inset: $sp-6 $sp-10 auto $sp-6;
display: flex;
flex-direction: column;
gap: $sp-4;
}
.page-mock__line {
display: block;
height: $sp-2;
border-radius: $r-xs;
background: rgba(#fff, .18);
}
.page-mock__line:nth-child(1) { width: 70%; }
.page-mock__line:nth-child(2) { width: 50%; }
.footer-mock {
position: absolute;
left: 0; right: 0; bottom: 0;
height: 64px;
background: $subject-ink;
transform: translateY(100%);
}
// 데모 루프 — 푸터가 올라오고, 버튼은 겹치지 않게 같은 박자로 위로 비켜준다 (캡처 주기 2s 고정)
.stage.is-demo .footer-mock { animation: footer-rise 2s $easing infinite; }
@keyframes footer-rise {
0%, 15% { transform: translateY(100%); }
55%, 100% { transform: translateY(0); }
}
.totop {
position: absolute;
right: $sp-6; bottom: $sp-6;
width: 48px; height: 48px;
border: 0;
border-radius: $r-pill;
background: $stage-paper;
display: grid;
place-items: center;
box-shadow: $shadow-raised;
cursor: pointer;
// 실제 사용: --lift 는 JS 가 IntersectionObserver 로 채운다
transform: translateY(calc(-1 * var(--lift, 0px)));
transition: transform $duration $easing;
}
.stage.is-demo .totop { animation: totop-lift 2s $easing infinite; }
@keyframes totop-lift {
0%, 15% { transform: translateY(0); }
55%, 100% { transform: translateY(-72px); }
}
.totop__arrow {
width: 22px; height: 22px;
fill: none;
stroke: $color;
stroke-width: 2.4;
stroke-linecap: round;
stroke-linejoin: round;
}
@media (prefers-reduced-motion: reduce) {
.footer-mock, .totop, .stage.is-demo .footer-mock, .stage.is-demo .totop { animation: none; transition: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.stage {
position: relative;
overflow: hidden;
}
.page-mock {
position: absolute;
inset: 24px 40px auto 24px;
display: flex;
flex-direction: column;
gap: 16px;
}
.page-mock__line {
display: block;
height: 8px;
border-radius: 4px;
background: rgba(255, 255, 255, 0.18);
}
.page-mock__line:nth-child(1) {
width: 70%;
}
.page-mock__line:nth-child(2) {
width: 50%;
}
.footer-mock {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 64px;
background: #17141a;
transform: translateY(100%);
}
.stage.is-demo .footer-mock {
animation: footer-rise 2s ease-out infinite;
}
@keyframes footer-rise {
0%, 15% {
transform: translateY(100%);
}
55%, 100% {
transform: translateY(0);
}
}
.totop {
position: absolute;
right: 24px;
bottom: 24px;
width: 48px;
height: 48px;
border: 0;
border-radius: 999px;
background: #ffffff;
display: grid;
place-items: center;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
cursor: pointer;
transform: translateY(calc(-1 * var(--lift, 0px)));
transition: transform 220ms ease-out;
}
.stage.is-demo .totop {
animation: totop-lift 2s ease-out infinite;
}
@keyframes totop-lift {
0%, 15% {
transform: translateY(0);
}
55%, 100% {
transform: translateY(-72px);
}
}
.totop__arrow {
width: 22px;
height: 22px;
fill: none;
stroke: #17141a;
stroke-width: 2.4;
stroke-linecap: round;
stroke-linejoin: round;
}
@media (prefers-reduced-motion: reduce) {
.footer-mock, .totop, .stage.is-demo .footer-mock, .stage.is-demo .totop {
animation: none;
transition: none;
}
}// 실제 사용: 푸터 센티널이 화면에 들어오면 버튼을 그만큼 translateY 로 밀어 올린다 (9줄)
var btn = document.querySelector('.totop');
var footer = document.querySelector('footer');
new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
var overlap = Math.max(0, e.intersectionRect.height);
btn.style.setProperty('--lift', overlap + 'px');
btn.classList.toggle('is-near-footer', e.isIntersecting);
});
}, { threshold: [0, 0.01, 1] }).observe(footer);import { useEffect, useRef, useState, type CSSProperties } from "react";
import styles from "./StopFooterButton.module.scss";
export type StopFooterButtonProps = {
/** 겹치지 않게 감시할 푸터 요소를 가리키는 선택자 (기본 "footer") */
footerSelector?: string;
duration?: number;
easing?: string;
};
type Vars = CSSProperties & { "--lift"?: string; "--duration"?: string; "--easing"?: string };
// 실제 사용: 푸터가 화면에 들어오면 겹친 높이만큼 translateY 로 버튼을 밀어 올린다
export default function StopFooterButton({
footerSelector = "footer",
duration = 2000,
easing = "ease-out",
}: StopFooterButtonProps) {
const [lift, setLift] = useState(0);
const observed = useRef(false);
useEffect(() => {
const footer = document.querySelector(footerSelector);
if (!footer || observed.current) return;
observed.current = true;
const io = new IntersectionObserver((entries) => {
entries.forEach((e) => setLift(Math.max(0, e.intersectionRect.height)));
}, { threshold: [0, 0.01, 1] });
io.observe(footer);
return () => io.disconnect();
}, [footerSelector]);
const vars: Vars = { "--lift": `${lift}px`, "--duration": `${duration}ms`, "--easing": easing };
return (
<button
type="button"
className={styles.totop}
style={vars}
aria-label="맨 위로"
onClick={() => window.scrollTo({ top: 0, behavior: "smooth" })}
>
<svg className={styles.arrow} viewBox="0 0 24 24" aria-hidden="true">
<polyline points="5,15 12,8 19,15" />
</svg>
</button>
);
}07 フッターで停止 — 計測 累積面積 24.521% · 動いたフレーム 11/23GODRICH オリジナル · SCSS と React は zip に同梱 · トップへ戻るボタン css アニメーション9選【CSSだけ・JS不要】
このデモが属する記事
トップへ戻るボタン css アニメーション9選【CSSだけ・JS不要】トップへ戻るボタンとは、ページを下まで読んだあとにワンクリックで一番上へ戻してくれる小さなボタンのことです。この記事では「トップへ戻るボタン css」でよく探される9つの動きを、AOS・GSAPなしでそのまま使えるコードとしてまとめました。 この記事のデモをすべて見る


