From dcb1e505fb8fc5b919ee2bcfc51dd675438806f0 Mon Sep 17 00:00:00 2001 From: andwati Date: Wed, 11 Mar 2026 21:26:43 +0300 Subject: [PATCH] feat: Add a scroll-to-top button with accompanying styles and JavaScript functionality. --- .prettierignore | 1 + TODO.md | 8 +----- sass/_feed.scss | 2 +- sass/_scroll-top.scss | 60 +++++++++++++++++++++++++++++++++++++++++ sass/main.scss | 1 + static/js/scroll-top.js | 34 +++++++++++++++++++++++ templates/base.html | 6 +++++ 7 files changed, 104 insertions(+), 8 deletions(-) create mode 100644 sass/_scroll-top.scss create mode 100644 static/js/scroll-top.js diff --git a/.prettierignore b/.prettierignore index 910a633..987dd0d 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,6 +4,7 @@ node_modules/ # Ignore Zola HTML templates (Tera syntax breaks standard HTML parsing) templates/**/*.html +templates/**/*.json # Ignore minified static files static/**/*.min.js diff --git a/TODO.md b/TODO.md index 600ee20..55a5505 100644 --- a/TODO.md +++ b/TODO.md @@ -33,7 +33,7 @@ Feature backlog for the blog. Check off items as they're implemented. ## UI / UX Polish -- [ ] **Scroll-to-top button** — floating button that appears after scrolling down, smooth-scrolls back to top +- [x] **Scroll-to-top button** — floating button that appears after scrolling down, smooth-scrolls back to top - [ ] **Table of contents active-section highlight** — as you scroll, the current heading is highlighted in the TOC - [ ] **Terminal-style command palette** — `Ctrl+K` opens a fuzzy-search palette over posts and nav links - [ ] **Animated hero terminal prompt** — typewriter effect on the homepage subtitle / profile section @@ -43,12 +43,6 @@ Feature backlog for the blog. Check off items as they're implemented. --- - - ## Distribution & Reach - [ ] **Newsletter / email subscription** — integrate Buttondown or self-hosted Listmonk with a subscribe form - - - - diff --git a/sass/_feed.scss b/sass/_feed.scss index 88b3dad..238ad6f 100644 --- a/sass/_feed.scss +++ b/sass/_feed.scss @@ -36,7 +36,7 @@ letter-spacing: 0.04em; &::after { - content: "→"; + content: '→'; transition: transform 0.2s ease; } diff --git a/sass/_scroll-top.scss b/sass/_scroll-top.scss new file mode 100644 index 0000000..2f48542 --- /dev/null +++ b/sass/_scroll-top.scss @@ -0,0 +1,60 @@ +// Scroll to Top Floating Button +.scroll-to-top { + position: fixed; + bottom: 2rem; + right: 2rem; + display: flex; + align-items: center; + justify-content: center; + width: 3rem; + height: 3rem; + border-radius: 50%; + background: var(--bg-color); + color: var(--accent-color); + border: 1px solid var(--accent-color); + cursor: pointer; + z-index: 100; + + // Hidden state by default + opacity: 0; + visibility: hidden; + transform: translateY(1rem); + transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); + + // Visible state triggered by JS + &.visible { + opacity: 1; + visibility: visible; + transform: translateY(0); + } + + @media (hover: hover) { + &:hover { + background: var(--accent-color); + color: var(--bg-color); + transform: translateY(-0.25rem); + box-shadow: 0 4px 12px rgba(16, 185, 129, 0.2); // Emerald glow matching accent + } + + &:hover svg { + transform: translateY(-2px); + } + } + + &:focus-visible { + outline: 2px solid var(--accent-color); + outline-offset: 4px; + } + + svg { + transition: transform 0.2s ease; + } + + // Responsive padding for mobile devices + @media (max-width: 768px) { + bottom: 1.5rem; + right: 1.5rem; + width: 2.75rem; + height: 2.75rem; + } +} diff --git a/sass/main.scss b/sass/main.scss index 55b45fa..7085875 100644 --- a/sass/main.scss +++ b/sass/main.scss @@ -13,3 +13,4 @@ @import 'footer'; @import 'search'; @import 'utilities'; +@import 'scroll-top'; diff --git a/static/js/scroll-top.js b/static/js/scroll-top.js new file mode 100644 index 0000000..f7a8ad7 --- /dev/null +++ b/static/js/scroll-top.js @@ -0,0 +1,34 @@ +document.addEventListener('DOMContentLoaded', () => { + const scrollToTopBtn = document.getElementById('scroll-to-top'); + + if (!scrollToTopBtn) return; + + // Show button when scrolled down 400px + const toggleVisibility = () => { + if (window.scrollY > 400) { + scrollToTopBtn.classList.add('visible'); + } else { + scrollToTopBtn.classList.remove('visible'); + } + }; + + // Smooth scroll to top when clicked + scrollToTopBtn.addEventListener('click', () => { + window.scrollTo({ + top: 0, + behavior: 'smooth', + }); + }); + + // Throttle the scroll event listener a tiny bit for performance + let isTicking = false; + window.addEventListener('scroll', () => { + if (!isTicking) { + window.requestAnimationFrame(() => { + toggleVisibility(); + isTicking = false; + }); + isTicking = true; + } + }); +}); diff --git a/templates/base.html b/templates/base.html index bafde57..d77445f 100644 --- a/templates/base.html +++ b/templates/base.html @@ -245,9 +245,15 @@ {% include "partials/footer.html" %} + + {% block scripts %}{% endblock scripts %}