Scroll Cue
A little mouse, its wheel rolling down, sits at the bottom of the hero to say there’s more below. It fades away as soon as you start scrolling and comes back when you return to the top. Tap it to glide down.
How to use it
- Paste the snippet just before
</head>. - Add an empty
<button data-scroll-cue>(or a link to the next section) inside your hero. It places itself at the bottom centre and draws the cue.
Options
Add any of these attributes to the same element.
data-style | mouse (default), arrow or chevrons. |
data-label | Small text under the cue, like Scroll. |
data-color | Its colour. Default the text colour. |
data-threshold | How far you scroll before it fades. Default 40 px. |
data-static | Leave its position to your own CSS. |
A button glides down one screen; a link glides to the section it points at. With reduced motion it stays still but still fades. Works inside scrolling boxes too.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Scroll Cue: a little mouse (its wheel rolling down) or a bouncing arrow
// sits at the bottom of the hero to say "there's more below", and fades
// away once you start scrolling. Back at the top, it comes back.
// Usage: <a href="#next" data-scroll-cue aria-label="Scroll down"></a>
// Put it inside the hero. It places itself at the bottom centre of its
// parent, draws the cue, and clicking it glides down a screen (a link
// glides to where it points instead).
// Optional: data-style ("mouse" (default), "arrow" or "chevrons"),
// data-label (small text under the cue, e.g. "Scroll"), data-color (any
// CSS colour, default the text colour), data-threshold (how many px of
// scrolling hides it, default 40), data-static (leave its positioning to
// your CSS).
// Works inside scrolling boxes too: it follows the nearest scrolling parent.
var DEFAULTS = { style: 'mouse', threshold: 40 };
var css =
'.scue{display:flex;flex-direction:column;align-items:center;gap:8px;text-decoration:none;cursor:pointer;transition:opacity .45s ease,transform .45s ease,visibility .45s}' +
'.scue-abs{position:absolute;left:50%;bottom:clamp(12px,4%,32px);transform:translateX(-50%)}' +
'.scue.scue-gone{opacity:0;visibility:hidden;pointer-events:none}' +
'.scue-abs.scue-gone{transform:translate(-50%,10px)}' +
'.scue:not(.scue-abs).scue-gone{transform:translateY(10px)}' +
'.scue-mouse{position:relative;width:24px;height:38px;border:2.5px solid currentColor;border-radius:13px;box-sizing:border-box}' +
'.scue-wheel{position:absolute;left:50%;top:7px;width:4px;height:8px;margin-left:-2px;border-radius:2px;background:currentColor;animation:scue-wheel 1.7s cubic-bezier(.6,0,.4,1) infinite}' +
'.scue-arrow{width:26px;height:26px;animation:scue-bob 1.5s cubic-bezier(.45,0,.55,1) infinite}' +
'.scue-chev{display:flex;flex-direction:column;align-items:center}' +
'.scue-chev svg{width:24px;height:14px;margin-top:-3px;animation:scue-chev 1.6s ease-in-out infinite}' +
'.scue-chev svg:nth-child(2){animation-delay:.18s}' +
'.scue-chev svg:nth-child(3){animation-delay:.36s}' +
'.scue-label{font-size:11px;font-weight:700;letter-spacing:.18em;text-transform:uppercase;opacity:.8}' +
'@keyframes scue-wheel{0%{transform:translateY(0);opacity:0}15%{opacity:1}70%{transform:translateY(12px);opacity:0}100%{transform:translateY(12px);opacity:0}}' +
'@keyframes scue-bob{0%,100%{transform:translateY(-3px)}50%{transform:translateY(7px)}}' +
'@keyframes scue-chev{0%,100%{opacity:.2}35%{opacity:1}}' +
'@media (prefers-reduced-motion:reduce){.scue-wheel,.scue-arrow,.scue-chev svg{animation:none}}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
var ARROW = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4v16M5 13l7 7 7-7"/></svg>';
var CHEVRON = '<svg viewBox="0 0 24 14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 3l8 8 8-8"/></svg>';
function num(v, fallback) { return v !== undefined && v !== '' && !isNaN(+v) ? +v : fallback; }
function scroller(el) {
for (var p = el.parentElement; p && p !== document.body; p = p.parentElement) {
var o = getComputedStyle(p).overflowY;
if (o === 'auto' || o === 'scroll') return p;
}
return null;
}
function setup(el) {
var d = el.dataset;
var kind = d.style || DEFAULTS.style;
var threshold = num(d.threshold, DEFAULTS.threshold);
var box = scroller(el);
var art = document.createElement('span');
art.setAttribute('aria-hidden', 'true');
if (kind === 'arrow') {
art.className = 'scue-arrow';
art.innerHTML = ARROW;
art.firstChild.style.cssText = 'display:block;width:100%;height:100%';
} else if (kind === 'chevrons') {
art.className = 'scue-chev';
art.innerHTML = CHEVRON + CHEVRON + CHEVRON;
} else {
art.className = 'scue-mouse';
art.innerHTML = '<span class="scue-wheel"></span>';
}
el.innerHTML = '';
el.appendChild(art);
if (d.label) {
var label = document.createElement('span');
label.className = 'scue-label';
label.textContent = d.label;
el.appendChild(label);
}
el.classList.add('scue');
if (d.color) el.style.color = d.color;
if (d.static === undefined) {
el.classList.add('scue-abs');
var parent = el.parentElement;
if (parent && getComputedStyle(parent).position === 'static') parent.style.position = 'relative';
}
if (!el.getAttribute('aria-label') && !el.closest('[aria-hidden="true"]')) el.setAttribute('aria-label', 'Scroll down');
// Clicking glides down: to the link's target, or one screen.
el.addEventListener('click', function (e) {
var href = el.getAttribute('href');
var target = href && href.charAt(0) === '#' && href.length > 1 ? document.querySelector(href) : null;
if (href && !target) return;
e.preventDefault();
var smooth = window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth';
if (target) target.scrollIntoView({ behavior: smooth, block: 'start' });
else if (box) box.scrollBy({ top: box.clientHeight * 0.9, behavior: smooth });
else window.scrollBy({ top: window.innerHeight * 0.9, behavior: smooth });
});
var gone = false;
function onScroll() {
var y = box ? box.scrollTop : window.pageYOffset;
var hide = y > threshold;
if (hide === gone) return;
gone = hide;
el.classList.toggle('scue-gone', hide);
}
onScroll();
(box || window).addEventListener('scroll', onScroll, { passive: true });
}
function init() {
document.querySelectorAll('[data-scroll-cue]').forEach(function (el) {
if (el._scue) return;
el._scue = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><section class="hero">
<h1>Hello there</h1>
<button type="button" data-scroll-cue data-label="Scroll" aria-label="Scroll down"></button>
</section>
<!-- a bouncing arrow linking to the next section: -->
<a href="#about" data-scroll-cue data-style="arrow" aria-label="Scroll down"></a>