Back To Top Button
A fixed back-to-top button that appears after the user scrolls a configurable distance using GSAP ScrollTrigger, animating in with a rotate and scale entrance. Includes a duplicate arrow for a CSS overflow-clip hover effect. Supports native scrollTo, Lenis, and Locomotive Scroll.
Setup — External Scripts
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/ScrollTrigger.min.js"></script>Code
<div data-back-to-top="wrap" class="back-top__wrap">
<button data-back-to-top="button" class="back-top__button">
<div class="back-top__arrow-wrap">
<div class="back-top__arrow-row">
<svg xmlns="http://www.w3.org/2000/svg" width="100%" viewbox="0 0 60 60" fill="none" class="back-top__arrow">
<path d="M47.5 25L30 7.5L12.5 25" stroke="currentColor" stroke-width="6" stroke-miterlimit="10" stroke-linecap="round"></path>
<path d="M30 7.5L30 55" stroke="currentColor" stroke-width="6" stroke-miterlimit="10" stroke-linecap="round"></path>
</svg>
</div>
<div class="back-top__arrow-row">
<svg xmlns="http://www.w3.org/2000/svg" width="100%" viewbox="0 0 60 60" fill="none" class="back-top__arrow">
<path d="M47.5 25L30 7.5L12.5 25" stroke="currentColor" stroke-width="6" stroke-miterlimit="10" stroke-linecap="round"></path>
<path d="M30 7.5L30 55" stroke="currentColor" stroke-width="6" stroke-miterlimit="10" stroke-linecap="round"></path>
</svg>
</div>
</div>
</button>
</div>.back-top__wrap {
z-index: 100;
pointer-events: none;
flex-flow: column;
justify-content: flex-end;
align-items: flex-end;
width: 100%;
height: 100vh;
padding: 2em;
display: flex;
position: fixed;
inset: 0%;
}
.back-top__button {
pointer-events: auto;
background-color: #fca5a0;
border: min(.5em, 5px) solid #efeeec;
border-radius: 1em;
outline-style: none;
width: max(5vw, 2.5rem);
height: max(5vw, 2.5rem);
padding: .5em;
position: relative;
}
.back-top__arrow {
width: 2.5em;
}
.back-top__arrow-wrap {
flex-flow: column;
justify-content: flex-start;
align-items: center;
width: 100%;
height: 100%;
display: flex;
position: relative;
overflow: hidden;
}
.back-top__arrow-row {
flex: none;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
display: flex;
}
/* Hide the back-to-top wrapper on default (to prevent flash on loaded site) */
[data-back-to-top="wrap"] { opacity: 0; }
.back-top__arrow-row {
transition: transform 0.5s cubic-bezier(.65, 0, 0, 1);
}
.back-top__button {
transition: border-width 0.5s cubic-bezier(.65, 0, 0, 1);
}
/* Keyboard focus state */
.back-top__button:focus-visible {
border-width: 0.6em;
}
.back-top__button:focus-visible .back-top__arrow-row {
transform: translate(0px, -100%);
}
/* Hover styling */
@media (hover: hover) {
.back-top__button:hover {
border-width: 0.6em;
}
.back-top__button:hover .back-top__arrow-row {
transform: translate(0px, -100%);
}
}
@media screen and (max-width: 991px) {
.back-top__wrap {
padding: 1.25em;
}
.back-top__button {
border-radius: .5em;
padding: .4em;
}
}gsap.registerPlugin(ScrollTrigger);
function initBackToTop() {
const buttonWrap = document.querySelector('[data-back-to-top="wrap"]');
const button = document.querySelector('[data-back-to-top="button"]');
if (!button || !buttonWrap) return;
// The minimum distance the page must be scrolled (in VH) for the button to appear
let minimumScrollDistance = 50;
// Un-do the initial CSS styling to hide the wrapper
gsap.set(buttonWrap, { autoAlpha: 1 });
// Hide the button itself
gsap.set(button, { autoAlpha: 0 });
ScrollTrigger.create({
trigger: document.body,
start: `top top-=${minimumScrollDistance}%`,
onEnter: () => {
gsap.fromTo(button, {
autoAlpha: 0,
rotate: -65,
scale: 0.4,
}, {
autoAlpha: 1,
rotate: 0,
scale: 1,
duration: 0.45,
ease: "power4.out"
});
},
onLeaveBack: () => {
gsap.to(button, {
autoAlpha: 0,
rotate: -65,
scale: 0.6,
duration: 0.4,
ease: "power4.out"
});
},
});
button.addEventListener('click', () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
});
}
// Initialize Back to Top Button
document.addEventListener("DOMContentLoaded", () => {
initBackToTop();
});Guide
Structure
Two elements are required: a fixed wrapper with data-back-to-top="wrap" (pointer-events: none so it doesn't block page interaction) and a button inside with data-back-to-top="button" (pointer-events: auto). The wrapper needs a high z-index to sit on top of other content.
Visibility on Load
The wrapper has opacity: 0 in CSS to prevent a flash of the button before JavaScript runs. The script uses gsap.set to restore it to autoAlpha: 1, then immediately hides the button itself, keeping it invisible until the scroll threshold is met.
Scroll Threshold
The minimumScrollDistance variable (default 50) controls how many percent of the viewport height the user must scroll before the button appears. Set it to 100 to require a full viewport scroll.
Arrow Hover Effect
The HTML includes two identical arrow SVGs stacked in .back-top__arrow-wrap with overflow: hidden. On hover or focus, both rows translate up by 100%, revealing the second arrow climbing in from below.
Scroll Alternatives
The default click handler uses window.scrollTo. For Lenis, replace it with: lenis.scrollTo(0, { lerp: 0.1 }). For Locomotive Scroll v5: locomotiveScroll.scrollTo(0, { lerp: 0.1 }).