Реалізація кнопки «Наверх» на сайті
Кнопка «Наверх» — мінімальний компонент з однією задачею: плавно прокрутити сторінку вгору та з'являтися тільки коли потрібна. Деталі реалізації впливають на продуктивність та доступність.
CSS + мінімальний JS
<button
class="back-to-top"
id="backToTop"
aria-label="Прокрутити вгору"
title="Наверх"
hidden
>
<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true">
<path d="M12 4l-8 8h5v8h6v-8h5z" fill="currentColor"/>
</svg>
</button>
.back-to-top {
position: fixed;
bottom: 32px;
right: 32px;
z-index: 50;
width: 44px;
height: 44px;
border-radius: 50%;
border: none;
background: #6366f1;
color: #fff;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 16px rgba(99, 102, 241, 0.4);
transition: opacity 0.3s, transform 0.3s, background 0.2s;
/* Попереджуємо Layout Shift — кнопка виведена з потоку */
}
/* hidden атрибут додає display:none — переопределяємо для анімації */
.back-to-top[hidden] {
display: flex !important;
opacity: 0;
pointer-events: none;
transform: translateY(8px);
}
.back-to-top:not([hidden]) {
opacity: 1;
transform: translateY(0);
}
.back-to-top:hover {
background: #4f46e5;
transform: translateY(-2px);
}
.back-to-top:active {
transform: translateY(0);
}
/* Мобільні: не перекриваємо нижню навігацію */
@media (max-width: 768px) {
.back-to-top {
bottom: calc(72px + env(safe-area-inset-bottom));
right: 16px;
width: 40px;
height: 40px;
}
}
const btn = document.getElementById('backToTop') as HTMLButtonElement
// Показуємо після 400px прокручування
const SHOW_THRESHOLD = 400
let ticking = false
window.addEventListener('scroll', () => {
if (ticking) return
ticking = true
requestAnimationFrame(() => {
btn.hidden = window.scrollY < SHOW_THRESHOLD
ticking = false
})
}, { passive: true })
btn.addEventListener('click', () => {
window.scrollTo({ top: 0, behavior: 'smooth' })
// Повертаємо фокус на перший фокусуваний елемент сторінки
const firstFocusable = document.querySelector<HTMLElement>(
'a[href], button:not([disabled]), [tabindex="0"]'
)
firstFocusable?.focus({ preventScroll: true })
})
React-компонент
import { useEffect, useState } from 'react'
export function BackToTop({ threshold = 400 }: { threshold?: number }) {
const [visible, setVisible] = useState(false)
useEffect(() => {
let ticking = false
const handler = () => {
if (ticking) return
ticking = true
requestAnimationFrame(() => {
setVisible(window.scrollY > threshold)
ticking = false
})
}
window.addEventListener('scroll', handler, { passive: true })
return () => window.removeEventListener('scroll', handler)
}, [threshold])
function scrollToTop() {
window.scrollTo({ top: 0, behavior: 'smooth' })
}
return (
<button
onClick={scrollToTop}
className={`back-to-top ${visible ? 'back-to-top--visible' : ''}`}
aria-label="Прокрутити вгору"
aria-hidden={!visible}
tabIndex={visible ? 0 : -1} // недоступна для Tab коли прихована
>
<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true">
<path d="M12 4l-8 8h5v8h6v-8h5z" fill="currentColor"/>
</svg>
</button>
)
}
Варіант з прогресом прокручування
Популярна варіація — кружок навколо кнопки, який показує відсоток прочитаного контенту:
function BackToTopWithProgress({ threshold = 400 }: { threshold?: number }) {
const [visible, setVisible] = useState(false)
const [progress, setProgress] = useState(0)
useEffect(() => {
const handler = () => {
const scrollY = window.scrollY
const maxScroll = document.documentElement.scrollHeight - window.innerHeight
setProgress(maxScroll > 0 ? (scrollY / maxScroll) * 100 : 0)
setVisible(scrollY > threshold)
}
window.addEventListener('scroll', handler, { passive: true })
return () => window.removeEventListener('scroll', handler)
}, [threshold])
const circumference = 2 * Math.PI * 18 // r=18
const dashOffset = circumference - (progress / 100) * circumference
return (
<button
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
className={`back-to-top-progress ${visible ? 'visible' : ''}`}
aria-label={`Прокрутити вгору. Прочитано ${Math.round(progress)}%`}
tabIndex={visible ? 0 : -1}
>
<svg viewBox="0 0 44 44" width="44" height="44">
{/* Фоновий круг */}
<circle cx="22" cy="22" r="18" fill="none" stroke="#e2e8f0" strokeWidth="3" />
{/* Прогрес */}
<circle
cx="22" cy="22" r="18"
fill="none"
stroke="#6366f1"
strokeWidth="3"
strokeDasharray={circumference}
strokeDashoffset={dashOffset}
strokeLinecap="round"
transform="rotate(-90 22 22)"
/>
{/* Стрілка вгору */}
<path d="M22 14l-6 6h4v8h4v-8h4z" fill="#6366f1" />
</svg>
</button>
)
}
Плавне прокручування: поведінка в браузерах
scroll-behavior: smooth в CSS робить кнопку ще простішою:
html {
scroll-behavior: smooth;
}
/* Але вимикаємо для користувачів з prefers-reduced-motion */
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
}
// Враховуємо prefers-reduced-motion в JS
function scrollToTop() {
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches
window.scrollTo({
top: 0,
behavior: prefersReduced ? 'instant' : 'smooth',
})
}
Графік
Кнопка з появленням/приховуванням та плавним прокручуванням — 1–2 години. З прогресс-кільцем та доступністю — пів дня.







