From 365d4ef258dec9986420d5581aab094ae4929c1f Mon Sep 17 00:00:00 2001 From: Pranav-IIITM Date: Sun, 8 Nov 2026 17:29:18 +0530 Subject: [PATCH] feat: add back-to-top button to How it Works page Add a floating button that appears after the hero section, shows reading progress with a ring that fills as the page scrolls, and smoothly returns to the top. It is smaller on phones and tablets and respects reduced motion. Signed-off-by: Pranav-IIITM --- themes/buildpacks/assets/js/how-it-works.js | 59 ++++++++++ .../assets/scss/components/_how-it-works.scss | 104 +++++++++++++++++- .../layouts/_default/how-it-works.html | 10 +- 3 files changed, 171 insertions(+), 2 deletions(-) diff --git a/themes/buildpacks/assets/js/how-it-works.js b/themes/buildpacks/assets/js/how-it-works.js index 22dfcae5f..375245df8 100644 --- a/themes/buildpacks/assets/js/how-it-works.js +++ b/themes/buildpacks/assets/js/how-it-works.js @@ -44,6 +44,65 @@ const round = (n) => Math.round(n * 100) / 100; const root = document.querySelector('[data-hiw]'); if (root) init(root); +const topButton = document.querySelector('[data-hiw-top]'); +if (topButton) initBackToTop(topButton); + +// Floating button that returns to the top; its ring fills with page progress +function initBackToTop(button) { + const ring = button.querySelector('[data-hiw-top-progress]'); + const hero = document.querySelector('.hiw-hero'); + const reduced = window.matchMedia('(prefers-reduced-motion: reduce)'); + let target = 0; + let shown = 0; + let running = false; + let then = 0; + + const paint = () => { + if (ring) ring.style.strokeDashoffset = (1 - shown).toFixed(4); + }; + + // The ring glides towards the scroll position instead of jumping with + // each wheel step + function frame(now) { + const dt = clamp((now - then) / 1000, 0, 0.1); + then = now; + shown += (target - shown) * (1 - Math.exp(-dt * 8)); + if (reduced.matches || Math.abs(target - shown) < 0.0005) shown = target; + paint(); + if (shown !== target) requestAnimationFrame(frame); + else running = false; + } + + function update() { + const scrolled = window.scrollY; + const max = document.documentElement.scrollHeight - window.innerHeight; + const threshold = hero ? hero.offsetTop + hero.offsetHeight : window.innerHeight * 0.5; + button.classList.toggle('is-visible', scrolled > threshold); + target = clamp(max > 0 ? scrolled / max : 0); + if (!running) { + running = true; + then = performance.now(); + requestAnimationFrame(frame); + } + } + + window.addEventListener('scroll', update, { passive: true }); + window.addEventListener('resize', update, { passive: true }); + update(); + // Start from where the page opened, e.g. a reload part-way down + shown = target; + paint(); + + button.addEventListener('click', (event) => { + event.preventDefault(); + window.scrollTo({ top: 0, behavior: reduced.matches ? 'auto' : 'smooth' }); + if (location.hash) history.replaceState(null, '', location.pathname + location.search); + // Return keyboard focus to the start of the page + const target = document.querySelector('a[href], button') || document.body; + target.focus({ preventScroll: true }); + }); +} + function init(root) { const steps = Array.from(root.querySelectorAll('[data-hiw-step]')); const railItems = Array.from(root.querySelectorAll('[data-hiw-rail]')); diff --git a/themes/buildpacks/assets/scss/components/_how-it-works.scss b/themes/buildpacks/assets/scss/components/_how-it-works.scss index 85cbb046b..fb073bea0 100644 --- a/themes/buildpacks/assets/scss/components/_how-it-works.scss +++ b/themes/buildpacks/assets/scss/components/_how-it-works.scss @@ -442,6 +442,93 @@ $hiw-red: #D9363E; } } + // Back to top: a floating cube-blue button whose ring tracks page progress + &-top { + position: fixed; + right: 1.5rem; + bottom: 1.5rem; + z-index: 1030; + display: flex; + align-items: center; + justify-content: center; + width: 52px; + height: 52px; + border-radius: 50%; + background: linear-gradient(145deg, $blue-light, $blue-dark); + box-shadow: 0 8px 22px rgba($blue-dark, 0.28); + opacity: 0; + visibility: hidden; + transform: translateY(20px) scale(0.8); + transition: + opacity 0.45s cubic-bezier(0.22, 1, 0.36, 1), + transform 0.55s cubic-bezier(0.22, 1, 0.36, 1), + visibility 0.45s, + box-shadow 0.35s ease; + + &.is-visible { + opacity: 1; + visibility: visible; + transform: none; + } + + &:hover, + &:focus-visible { + box-shadow: 0 12px 28px rgba($pink, 0.35); + transform: translateY(-3px); + + .hiw-top__arrow { + transform: translateY(-3px) rotate(45deg); + } + + .hiw-top__track { + stroke: rgba($white, 0.35); + } + } + + &:focus-visible { + outline: 2px solid $pink; + outline-offset: 3px; + } + + &__ring { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + transform: rotate(-90deg); + } + + &__track, + &__fill { + fill: none; + stroke-width: 3; + } + + &__track { + stroke: rgba($white, 0.18); + transition: stroke 0.25s ease; + } + + &__fill { + stroke: $pink; + stroke-linecap: round; + stroke-dasharray: 1; + stroke-dashoffset: 1; + } + + // Chevron pointing up, nudged down so it sits optically centred + &__arrow { + width: 12px; + height: 12px; + margin-top: 5px; + border-top: 2.5px solid $white; + border-left: 2.5px solid $white; + border-radius: 2px 0 0 0; + transform: rotate(45deg); + transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1); + } + } + // Phones and tablets: the scene fills the screen and the steps // scroll over it as cards @media (max-width: $hiw-compact) { @@ -502,6 +589,19 @@ $hiw-red: #D9363E; } } + &-top { + right: 1rem; + bottom: 1rem; + width: 44px; + height: 44px; + + &__arrow { + width: 10px; + height: 10px; + margin-top: 4px; + } + } + &-rail { ol { flex-wrap: nowrap; @@ -521,7 +621,9 @@ $hiw-red: #D9363E; animation: none !important; } - &--ready .hiw-step__card { + &--ready .hiw-step__card, + &-top, + &-top__arrow { transition: none; } } diff --git a/themes/buildpacks/layouts/_default/how-it-works.html b/themes/buildpacks/layouts/_default/how-it-works.html index 175abf4b5..6f90d5b30 100644 --- a/themes/buildpacks/layouts/_default/how-it-works.html +++ b/themes/buildpacks/layouts/_default/how-it-works.html @@ -1,7 +1,7 @@ {{ define "main" }}
-
+

Under the hood

How Buildpacks Work

@@ -84,6 +84,14 @@

Rebase

+ + + + + {{ $script := resources.Get "js/how-it-works.js" | minify | fingerprint }}