GODRICH
#css애니메이션#버튼#로딩#전환효과#스크롤#대시보드#실전ui#디자인트렌드

위로가기 버튼 CSS 애니메이션 9종 — 스크롤하면 나타나는

위로가기 버튼은 스크롤로 한참 내려간 화면에서 다시 맨 위로 데려다주는 작은 버튼입니다. AOS·GSAP 없이, JS 0줄로 되는 것부터 스크롤 위치를 직접 읽는 것까지 아홉 가지를 모았고, 그중 하나는 애니메이션 없이 scroll-behavior 한 줄만으로 화면이 매끄럽게

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

아홉 개는 이름 가나다순이 아니라 버튼이 화면에서 겪는 순서로 놓았습니다. 먼저 버튼이 등장하는 방식(01~03), 그다음 눈길을 끄는 아이디어형 모양(04·06), 등장의 다른 갈래(05), 화면 아래쪽과 부딪히지 않게 처신하는 법(07), 손을 올렸을 때의 반응(08), 마지막으로 애니메이션 없이 브라우저에 맡기는 방법(09)입니다. 내려가는 길이 이야기를 품고 있다면 타임라인 애니메이션이 그 서사를 싣고, 한 화면을 가로로 넘기는 대안은 CSS 슬라이더 캐러셀이 마련합니다.

항목 기법 시간 트리거
01 스크롤하면 등장 opacity+translateY 260ms 스크롤(JS)
02 화살표 바운스 translateY 1.4s 상시
03 진행 링 표시 stroke-dashoffset 2s(데모) 스크롤(JS)
04 로켓 모양 translateY+rotate 700ms 호버·포커스
05 옆에서 슬라이드 translateX+@starting-style 320ms 스크롤(JS)
06 회전하는 텍스트 rotate 8s 상시
07 푸터에서 멈춤 translateY 220ms 스크롤(JS)
08 호버 시 필 확장 width 220ms 호버·포커스
09 scroll-behavior (네이티브 스크롤) — 클릭

01스크롤하면 등장

스크롤 탑버튼은 언제 나타나나

버튼은 평소 opacity:0으로 숨어 있다가, 스크롤이 240px를 넘으면 자바스크립트 7줄이 .is-visible 클래스를 더해 서서히 떠오릅니다.

스크롤하면 등장
새 창에서 열기

01 · 스크롤하면 등장 — 측정 누적면적 1.764% · 움직인 프레임 9/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

scroll 이벤트opacity+translateY260ms

짚어 둘 점

긴 목록이나 블로그 글처럼 스크롤이 긴 페이지 하단에 가장 흔히 쓰는 방식입니다.

$duration: 260ms;
$easing: ease-out;

.totop {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity $duration $easing, transform $duration $easing;
}
.totop.is-visible { opacity: 1; transform: translateY(0); }
var btn = document.querySelector('.totop');
function sync() { btn.classList.toggle('is-visible', window.scrollY > 240); }
window.addEventListener('scroll', sync, { passive: true });
sync();

02화살표 바운스

눈에 보이는 것

버튼 배경은 고정한 채 안쪽 화살표 아이콘만 위아래로 계속 통통 튑니다.

화살표 바운스
새 창에서 열기

02 · 화살표 바운스 — 측정 누적면적 0.17% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

translateY$ease-out1.4s

어떻게 만들었나

자바스크립트 없이 CSS keyframes 하나로 "여기를 누르면 위로 간다"는 걸 말없이 알려줍니다.

.totop__arrow {
  animation: arrow-bounce 1.4s ease-out infinite;
}
@keyframes arrow-bounce {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-5px); }
}

03진행 링 표시

어떤 상황인가

버튼 테두리를 SVG 원이 감싸고, stroke-dashoffset이 스크롤한 비율만큼 링을 채웁니다.

진행 링 표시
새 창에서 열기

03 · 진행 링 표시 — 측정 누적면적 0.454% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

stroke-dashoffset@propertyJS 7줄

코드 읽는 법

@property --p를 inherits: true로 등록해 부모 버튼에서 바뀐 값을 자식 SVG 원이 그대로 물려받게 한 게 핵심입니다.

@property --p {
  syntax: '<number>';
  initial-value: 0;
  inherits: true;
}
.totop { --p: 0; }
.totop__fill {
  stroke-dasharray: 150.8px;
  stroke-dashoffset: calc(150.8px * (1 - var(--p) / 100));
}
var max = document.documentElement.scrollHeight - window.innerHeight;
var ratio = max > 0 ? Math.min(100, (window.scrollY / max) * 100) : 0;
el.style.setProperty('--p', ratio);

04로켓 모양

무슨 일이 일어나나

버튼 자체가 로켓 아이콘이고, 마우스를 올리거나 키보드로 포커스하면 몸체가 기울며 살짝 솟구치고 뒤에서 불꽃이 깜빡입니다.

로켓 모양
새 창에서 열기

04 · 로켓 모양 — 측정 누적면적 0.289% · 움직인 프레임 10/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

rotate+translateYopacity 깜빡임700ms

코드의 요점

재미 요소가 필요한 게임·스타트업 랜딩 페이지에 어울립니다.

.totop:hover .totop__rocket,
.totop:focus-visible .totop__rocket { animation: rocket-launch 700ms ease-out 1; }
.totop:hover .totop__flame,
.totop:focus-visible .totop__flame { animation: flame-flicker 700ms ease-out 1; }

@keyframes rocket-launch {
  0%   { transform: translateY(0) rotate(0deg); }
  40%  { transform: translateY(-9px) rotate(-9deg); }
  100% { transform: translateY(-2px) rotate(-4deg); }
}
@keyframes flame-flicker {
  0%, 100% { opacity: 0; transform: scaleY(.6); }
  40%      { opacity: 1; transform: scaleY(1.3); }
  70%      { opacity: .7; transform: scaleY(.9); }
}

05옆에서 슬라이드

화면에서 벌어지는 일

버튼이 화면 오른쪽 바깥에서 안으로 미끄러지듯 들어옵니다.

옆에서 슬라이드
새 창에서 열기

05 · 옆에서 슬라이드 — 측정 누적면적 2.232% · 움직인 프레임 8/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

translateX@starting-style320ms

동작 원리

display: none에서 바뀔 때 진입 시작값을 정해 주는 @starting-style을 써서, 자바스크립트는 스크롤 위치만 확인하고 등장 모양은 CSS가 전담합니다.

.totop {
  display: none;
  opacity: 0;
  transform: translateX(40px);
  transition: opacity 320ms, transform 320ms, display 320ms allow-discrete;
}
.totop.is-visible {
  display: grid;
  opacity: 1;
  transform: translateX(0);
  @starting-style { opacity: 0; transform: translateX(40px); }
}

06회전하는 텍스트

어떻게 보이나

버튼 테두리를 따라 "SCROLL TO TOP" 글자가 SVG textPath로 원형 배치돼 천천히 계속 돌고, 가운데 화살표만 따로 고정돼 있습니다.

회전하는 텍스트
새 창에서 열기

06 · 회전하는 텍스트 — 측정 누적면적 0.895% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SVG textPathrotate8s

만드는 방식

브랜드 개성이 강한 사이트나 포트폴리오 하단에 어울립니다.

.totop__ring {
  animation: ring-spin 8s linear infinite;
}
@keyframes ring-spin {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}
<text><textPath href="#ring-path">SCROLL TO TOP · SCROLL TO TOP · </textPath></text>

07푸터에서 멈춤

언제 쓰나

평소엔 화면에 붙어 떠 있다가, 푸터가 올라오면 그 앞을 가리지 않도록 translateY로 위치를 살짝 올려 멈춥니다.

푸터에서 멈춤
새 창에서 열기

07 · 푸터에서 멈춤 — 측정 누적면적 24.521% · 움직인 프레임 11/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

IntersectionObservertranslateYJS 9줄

왜 이렇게 만들었나

IntersectionObserver 9줄이 푸터와 겹친 높이를 CSS 변수 --lift에 대입할 뿐, 애니메이션은 여전히 transform 하나로 처리합니다.

.totop {
  transform: translateY(calc(-1 * var(--lift, 0px)));
  transition: transform 220ms ease-out;
}
new IntersectionObserver(function (entries) {
  entries.forEach(function (e) {
    var overlap = Math.max(0, e.intersectionRect.height);
    btn.style.setProperty('--lift', overlap + 'px');
  });
}, { threshold: [0, 0.01, 1] }).observe(footer);

08호버 시 필 확장

어떤 화면인가

평소엔 동그란 아이콘 버튼이다가 마우스를 올리면 옆으로 늘어나며 "맨 위로" 글자가 함께 드러납니다.

호버 시 필 확장
새 창에서 열기

08 · 호버 시 필 확장 — 측정 누적면적 5.592% · 움직인 프레임 15/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

width transition:hover·:focus-visible220ms

구현의 핵심

width 전환이라 top·left 같은 값보다는 레이아웃을 더 건드리지만, 버튼 하나만 다시 그리므로 비용이 작습니다.

.totop {
  width: 48px;
  overflow: hidden;
  transition: width 220ms ease-out;
}
.totop:hover,
.totop:focus-visible { width: 132px; }

09scroll-behavior 부드러운 이동

무엇이 움직이나

앞의 여덟 개와 달리 이 항목엔 애니메이션도 자바스크립트도 없습니다.

scroll-behavior 부드러운 이동
새 창에서 열기

09 · scroll-behavior 부드러운 이동 — 측정 누적면적 50.129% · 움직인 프레임 11/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

href=#mp-topJS 0줄앵커 링크

무엇이 핵심인가

html에 scroll-behavior: smooth 한 줄만 넣으면 앵커 링크를 누른 화면이 브라우저가 알아서 매끄럽게 위로 움직입니다.

html { scroll-behavior: smooth; }

.totop {
  text-decoration: none;
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}
<a href="#top" class="totop" aria-label="맨 위로">
  <svg viewBox="0 0 24 24" aria-hidden="true">
    <polyline points="5,15 12,8 19,15" />
  </svg>
</a>

어디서 깨지는가 — @property를 부모에서만 쓰면 자식이 못 읽는다

무엇이 문제인가

back to top button css 를 아홉 가지로 나눠 만들면서, 03 진행 링 표시를 처음 만들 때 --p를 inherits: false로 등록했다가 누적면적이 0.00%로 통째로 실패했습니다. 애니메이션은 부모 버튼(.totop)에서 도는데, 실제로 링을 그리는 stroke-dashoffset은 자식 SVG 원(.totop__fill)에 있었고, inherits: false라 자식이 부모의 바뀐 값을 물려받지 못해 링이 계속 0%로 멈춰 있었던 것입니다. inherits: true로 고치자 바로 움직였습니다.

어떻게 피하나

버튼이 나타나는 순간을 놓치면 위로가기 화살표가 사라진 채 남는데, 등장·퇴장 타이밍을 프레임 단위로 다루는 CSS 화살표 애니메이션이 그 지점을 짚습니다.

07 푸터에서 멈춤도 비슷한 함정이 있는데, 데모는 실제 스크롤 없이 footer-mock과 버튼을 같은 타이밍의 keyframes로 동시에 움직여야 하고, 하나라도 박자가 어긋나면 버튼이 푸터를 뚫고 지나가는 것처럼 보입니다. 압축 비밀번호 9sbsj52d로 여는 zip에는 아홉 개 모두 duration·easing을 맞춘 vanilla·React 파일이 들어 있습니다.

접근성

reduced-motion 에서 남는 것

운영체제에 동작 줄이기 설정이 켜져 있으면 아홉 개 모두 @media (prefers-reduced-motion: reduce) 블록이 움직임을 지웁니다. 01·05·07처럼 등장·이동 자체가 정보인 항목은 reduced-motion에서도 최종 위치(보이는 상태·멈춘 위치)로 고정돼 있어 존재를 잃지 않고, 02·03·04·06처럼 장식성 반복 동작은 지워도 버튼의 클릭 기능이 그대로 남습니다. 09는 scroll-behavior: smooth 자체가 이미 reduced-motion 설정을 브라우저가 존중하는 값이라 별도 처리가 필요 없습니다. 이 미디어 쿼리의 브라우저 지원 범위는 MDN의 prefers-reduced-motion 문서에서 확인할 수 있고, scroll-behavior의 지원 범위는 caniuse에서 확인할 수 있습니다.

이어서 볼 글

이 시리즈의 다른 묶음은 CSS 애니메이션 카테고리에서 모아 보고, 스크롤에 반응하는 인디케이터를 더 찾는다면 스크롤 진행률 표시 9종도 함께 참고하십시오. 어떤 사이트인지 궁금하면 소개 페이지에 제작 방식을 적어 뒀습니다.

FAQ

맨 위로 가기 버튼은 몇 px 스크롤해야 나타나야 합니까?

정해진 값은 없지만 화면 높이(뷰포트)의 절반에서 한 번 정도가 많이 쓰입니다. 이 글의 데모는 240px로 고정했는데, 실제 페이지에서는 window.innerHeight를 기준으로 계산해 화면 크기가 달라도 같은 비율에서 나타나게 하는 편이 낫습니다.

09처럼 scroll-behavior만 쓰면 다른 항목이 다 필요 없습니까?

아닙니다. scroll-behavior: smooth는 "어떻게 올라갈지"만 정할 뿐 "버튼이 언제 나타날지", "푸터와 겹치지 않을지" 같은 문제는 그대로 남습니다. 09는 01~08과 경쟁하는 게 아니라, 01~08 중 하나로 등장을 처리한 뒤 실제 스크롤 동작에 얹어 쓰는 조합이 흔합니다.

React 컴포넌트에서도 IntersectionObserver를 그대로 씁니까?

네, 07의 React 판(StopFooterButton)도 useEffect 안에서 같은 IntersectionObserver를 만들어 푸터 요소를 감시합니다. 컴포넌트가 언마운트될 때 disconnect()로 정리하는 것만 vanilla 판과 다릅니다. 맨 위로 돌아가는 길을 포함해 페이지 전체의 흐름을 꾸미려면 홈페이지 스크롤 효과가 그 큰 틀입니다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.