Widget:ScrollToTopSidebar: Difference between revisions

From Bodhicitta
(Scroll-to-top fill starts when the button comes into view, not at the top of the content)
(Scroll-to-top: floating pill at the bottom centre of the viewport, progress fill from when it appears)
Line 1: Line 1:
<style>
<style>
/* A darker band fills the button from the left as you read down the content column (--stt-p, set below). */
/* Floating pill at the bottom centre of the viewport. A darker band fills it from the
  left as you read down the content column (--stt-p, set below); the off-white base
  keeps it legible over the text it floats above. */
#scrollToTop {
#scrollToTop {
background: linear-gradient(to right, rgba(186, 27, 29, .3) var(--stt-p, 0%), rgba(186, 27, 29, .1) var(--stt-p, 0%));
position: fixed;
left: 50%;
bottom: 1.5rem;
transform: translateX(-50%);
width: 11rem;
height: 44px;
border-radius: 22px;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0, 0, 0, .18);
background:
linear-gradient(to right, rgba(186, 27, 29, .3) var(--stt-p, 0%), rgba(186, 27, 29, .1) var(--stt-p, 0%)),
#fffcf5;
}
}
#scrollToTop:hover {
#scrollToTop:hover {
background: linear-gradient(to right, rgba(186, 27, 29, .5) var(--stt-p, 0%), rgba(186, 27, 29, .3) var(--stt-p, 0%));
background:
linear-gradient(to right, rgba(186, 27, 29, .5) var(--stt-p, 0%), rgba(186, 27, 29, .3) var(--stt-p, 0%)),
#fffcf5;
}
@media (max-width: 767.98px) {
#scrollToTop { display: none !important; }
}
}
</style>
</style>
Line 14: Line 32:
scrollToTopButtonSb.setAttribute('id', 'scrollToTop');
scrollToTopButtonSb.setAttribute('id', 'scrollToTop');


// Add the button to the document body
// The button floats over the page, so it lives on the body; the sidebar slot that
// invokes this widget is left empty and hidden.
const container = document.getElementById('scrollToTopContainer');
const container = document.getElementById('scrollToTopContainer');
container.appendChild(scrollToTopButtonSb);
document.body.appendChild(scrollToTopButtonSb);
if (container) container.style.display = 'none';


// Handle initial hash navigation
// Handle initial hash navigation
Line 30: Line 50:
});
});


// Progress runs from the moment the button comes into view (it sits at the foot of a
// Progress runs from the moment the button appears (STT_SHOW) to the end of the main
// tall sticky sidebar, so that can be well down the page) to the end of the main
// content column (the .col-md-9 beside the sidebar, or the whole page).
// content column beside the sidebar (the .col-md-9 sibling, or the whole page).
const STT_SHOW = 300;
const sttRow = container.closest('.row');
const sttRow = container && container.closest('.row');
const sttContent = (sttRow && sttRow.querySelector(':scope > .col-md-9')) || document.documentElement;
const sttContent = (sttRow && sttRow.querySelector(':scope > .col-md-9')) || document.documentElement;
let sttStart = 300;


function sttProgress() {
function sttProgress() {
const y = window.pageYOffset, vh = window.innerHeight;
const y = window.pageYOffset;
// While the button is still below the fold, estimate the scroll position at which it
const end = y + sttContent.getBoundingClientRect().bottom - window.innerHeight;
// will be fully in view; once visible (the sidebar sticks), keep that estimate.
if (end <= STT_SHOW) return 1;
const btnBottom = container.getBoundingClientRect().top + (scrollToTopButtonSb.offsetHeight || 50);
return Math.min(1, Math.max(0, (y - STT_SHOW) / (end - STT_SHOW)));
if (btnBottom > vh) sttStart = Math.max(300, y + btnBottom - vh);
const end = y + sttContent.getBoundingClientRect().bottom - vh;
if (end <= sttStart) return 1;
return Math.min(1, Math.max(0, (y - sttStart) / (end - sttStart)));
}
}


Line 52: Line 67:
function sttUpdate() {
function sttUpdate() {
sttTicking = false;
sttTicking = false;
if (window.pageYOffset > 300) {
if (window.pageYOffset > STT_SHOW) {
scrollToTopButtonSb.style.display = 'block';
scrollToTopButtonSb.style.display = 'block';
scrollToTopButtonSb.style.setProperty('--stt-p', (sttProgress() * 100).toFixed(2) + '%');
scrollToTopButtonSb.style.setProperty('--stt-p', (sttProgress() * 100).toFixed(2) + '%');

Revision as of 18:25, 30 September 2026

<style> /* Floating pill at the bottom centre of the viewport. A darker band fills it from the

  left as you read down the content column (--stt-p, set below); the off-white base
  keeps it legible over the text it floats above. */
  1. scrollToTop {

position: fixed; left: 50%; bottom: 1.5rem; transform: translateX(-50%); width: 11rem; height: 44px; border-radius: 22px; overflow: hidden; box-shadow: 0 2px 10px rgba(0, 0, 0, .18); background: linear-gradient(to right, rgba(186, 27, 29, .3) var(--stt-p, 0%), rgba(186, 27, 29, .1) var(--stt-p, 0%)), #fffcf5; }

  1. scrollToTop:hover {

background: linear-gradient(to right, rgba(186, 27, 29, .5) var(--stt-p, 0%), rgba(186, 27, 29, .3) var(--stt-p, 0%)), #fffcf5; } @media (max-width: 767.98px) { #scrollToTop { display: none !important; } } </style> <script> // Create the scroll to top button const scrollToTopButtonSb = document.createElement('button'); scrollToTopButtonSb.innerHTML = 'to top '; scrollToTopButtonSb.setAttribute('id', 'scrollToTop');

// The button floats over the page, so it lives on the body; the sidebar slot that // invokes this widget is left empty and hidden. const container = document.getElementById('scrollToTopContainer'); document.body.appendChild(scrollToTopButtonSb); if (container) container.style.display = 'none';

// Handle initial hash navigation window.addEventListener('load', () => { if (window.location.hash) { setTimeout(() => { const element = document.querySelector(window.location.hash); if (element) { element.scrollIntoView({ behavior: 'smooth' }); } }, 100); } });

// Progress runs from the moment the button appears (STT_SHOW) to the end of the main // content column (the .col-md-9 beside the sidebar, or the whole page). const STT_SHOW = 300; const sttRow = container && container.closest('.row'); const sttContent = (sttRow && sttRow.querySelector(':scope > .col-md-9')) || document.documentElement;

function sttProgress() { const y = window.pageYOffset; const end = y + sttContent.getBoundingClientRect().bottom - window.innerHeight; if (end <= STT_SHOW) return 1; return Math.min(1, Math.max(0, (y - STT_SHOW) / (end - STT_SHOW))); }

// Show/hide the button and advance its fill with scroll position let sttTicking = false; function sttUpdate() { sttTicking = false; if (window.pageYOffset > STT_SHOW) { scrollToTopButtonSb.style.display = 'block'; scrollToTopButtonSb.style.setProperty('--stt-p', (sttProgress() * 100).toFixed(2) + '%'); } else { scrollToTopButtonSb.style.display = 'none'; } } window.addEventListener('scroll', () => { if (!sttTicking) { sttTicking = true; window.requestAnimationFrame(sttUpdate); } }, { passive: true });

// Scroll to top when the button is clicked scrollToTopButtonSb.addEventListener('click', () => { window.scrollTo({ top: 0, behavior: 'smooth' }); }); </script>