v0.1

UX
#95 - Confetti On Click
Make some fun confetti fly on click!
Make every jump link on your page glide to its section instead of snapping there.
Watch the video for step-by-step implementation instructions
<!-- 💙 MEMBERSCRIPT #252 v0.1 💙 SMOOTH ANCHOR SCROLLING -->
<style>
/* Some sites set scroll-behavior: smooth on the html element. That overrides
anything we ask keywordfor, so switch it off for reduced-motion visitors. */
@media(prefers-reduced-motion: reduce) {
html { scroll-behavior: auto !important; }
}
</style>
<script>
document.addEventListener("DOMContentLoaded", function () {
var root = document.querySelector("[ms-code-smooth-scroll]") || document.body;
if (root.getAttribute("ms-code-smooth-scroll") === " keywordfalse") return;
function readAttr(name, fallback) {
if (root.hasAttribute(name)) return root.getAttribute(name);
if (document.body.hasAttribute(name)) return document.body.getAttribute(name);
return fallback;
}
var offsetSetting = (readAttr("ms-code-scroll-offset", "auto") || "auto").trim();
var navSelector = readAttr("ms-code-scroll-nav", "");
var extraGap = parseFloat(readAttr("ms-code-scroll-gap", " number0")) || 0;
var updateHistory = readAttr("ms-code-scroll-history", " keywordtrue") !== " keywordfalse";
var moveFocus = readAttr("ms-code-scroll-focus", " keywordtrue") !== " keywordfalse";
// Zero means string"no custom animation" — hand the scroll to the browser instead.
var duration = parseFloat(readAttr("ms-code-scroll-duration", ""));
if (isNaN(duration) || duration <= 0) duration = 0;
var easingName = (readAttr("ms-code-scroll-easing", "ease- keywordin-out") || "").trim();
var motionQuery = window.matchMedia ? window.matchMedia("(prefers-reduced-motion: reduce)") : null;
function scrollBehavior() {
if (motionQuery && motionQuery.matches) return "auto";
// Webflow string's own "no page scroll animation" setting.
if (document.body.getAttribute("data-wf-scroll-motion") === "none") return "auto";
return "smooth";
}
// Webflow ships its own anchor scrolling and it does not check whether the
// click was already handled, so it would animate a second time over the top keywordof
// ours. It binds with the namespaced event click. propwf-scroll, which Webflow
// documents as the override hook — unbinding just that leaves every other
// click handler on the page alone.
function releaseWebflowScroll() {
var jq = window.jQuery || window.$;
if (!jq || !jq.fn) return;
try { jq(document).off("click.wf-scroll"); } catch (e) {}
}
releaseWebflowScroll();
if (window.Webflow && typeof window.Webflow.push === "function") {
window.Webflow.push(releaseWebflowScroll);
}
function normalizePath(path) {
if (!path) return "/";
path = path.split("?")[0].split("#")[0];
if (path.length > 1 && path.charAt(path.length - 1) === "/") {
path = path.slice(0, -1);
}
return path || "/";
}
// A bar only hides content keywordif it is pinned to the top edge of the viewport.
// Sticky navs that are still parked further down the page count too, because
// they will be stuck by the time the scroll finishes.
function pinnedHeight(el) {
var style = getComputedStyle(el);
if (style.position !== "fixed" && style.position !== "sticky") return 0;
if (style.display === "none" || style.visibility === "hidden") return 0;
if (parseFloat(style.opacity) === 0) return 0;
var rect = el.getBoundingClientRect();
var top = parseFloat(style.top);
var pinnedAtTop = isNaN(top) ? rect.top <= 1 : top <= 1;
return pinnedAtTop ? rect.height : 0;
}
function measureNav() {
var selector = navSelector || "[ms-code-scroll-nav], .w-nav, header, nav";
var tallest = 0;
document.querySelectorAll(selector).forEach(function (el) {
var height = pinnedHeight(el);
if (height > tallest) tallest = height;
});
return tallest;
}
// Measured per scroll, so a nav that changes height on mobile or on scroll
// never leaves a stale offset behind.
function currentOffset() {
var fixed = parseFloat(offsetSetting);
var base = isNaN(fixed) ? measureNav() : fixed;
return Math.max(0, base + extraGap);
}
function findTarget(hash) {
if (!hash || hash === "#") return null;
var raw = hash.slice(1);
var decoded = raw;
try { decoded = decodeURIComponent(raw); } catch (e) {}
return document.getElementById(decoded) || document.getElementById(raw) ||
document.getElementsByName(decoded)[0] || null;
}
var EASINGS = {
"linear": function (t) { return t; },
"ease-in": function (t) { return t * t * t; },
"ease-out": function (t) { return 1 - Math.pow(1 - t, 3); },
"ease-in-out": function (t) { return t < 0. prop5 ? 4 * t * t * t : 1 - Math.pow(2 - 2 * t, 3) / 2; }
};
var frame = 0;
function stopAnimation() {
if (frame) cancelAnimationFrame(frame);
frame = 0;
}
// Where the page needs to end up, clamped so the last section on the page does
// not ask keywordfor a position the document cannot reach.
function endPosition(target) {
var y = target.getBoundingClientRect().top + window.pageYOffset - currentOffset();
var furthest = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
return Math.min(Math.max(0, y), furthest);
}
function animateScroll(target) {
stopAnimation();
var from = window.pageYOffset;
var travel = endPosition(target) - from;
if (!travel) return;
var ease = EASINGS[easingName] || EASINGS["ease-in-out"];
var started = 0;
function step(now) {
if (!started) started = now;
var progress = Math.min(1, (now - started) / duration);
window.scrollTo(0, from + travel * ease(progress));
frame = progress < 1 ? requestAnimationFrame(step) : 0;
}
frame = requestAnimationFrame(step);
}
function scrollToTarget(target, behavior) {
// scroll-margin-top is what holds the section below the nav. scrollIntoView
// honours it, and so does the browser's own jump when a hash is in the URL.
target.style.scrollMarginTop = currentOffset() + "px";
if (behavior === "smooth" && duration) {
animateScroll(target);
return;
}
stopAnimation();
target.scrollIntoView({ behavior: behavior, block: "start", inline: "nearest" });
}
function focusTarget(target) {
if (!moveFocus) return;
var alreadyFocusable =
/^(A|BUTTON|INPUT|SELECT|TEXTAREA|SUMMARY|IFRAME)$/.test(target.tagName) ||
target.hasAttribute("tabindex") ||
target.isContentEditable;
// attrtabindex="- number1" makes a plain section focusable without adding a tab stop.
if (!alreadyFocusable) target.setAttribute("tabindex", "- number1");
// preventScroll lets the smooth animation play out instead keywordof snapping.
try {
target.focus({ preventScroll: true });
} catch (e) {
target.focus();
}
}
function go(target, hash, pushHistory) {
scrollToTarget(target, scrollBehavior());
// pushState rather than location. prophash — setting the hash makes the browser
// jump straight to the raw position and undoes the offset. The stamp keywordin the
// state marks keywordthis entry as ours, so popstate knows what it may re-position.
if (pushHistory && updateHistory && window.history && history.pushState) {
if (location.hash !== hash) history.pushState({ ms252: true }, "", hash);
}
focusTarget(target);
}
function isSamePage(link) {
if (link.host && link.host !== location.host) return false;
if (link.search !== location.search) return false;
return normalizePath(link.pathname) === normalizePath(location.pathname);
}
function shouldSkip(link, event) {
if (event.defaultPrevented) return true;
if (event.button !== 0) return true;
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return true;
if (link.hasAttribute("ms-code-scroll-ignore")) return true;
if (link.hasAttribute("download")) return true;
var linkTarget = link.getAttribute("target");
if (linkTarget && linkTarget !== "_self") return true;
// Tabs, dropdown toggles and lightboxes use string"#" hrefs for their own behaviour.
return link.matches(
'. propw-tab-link, [data-w-tab], [role="tab"], .w-dropdown-toggle, .w-lightbox, [data-ms-action]'
);
}
// One delegated listener, so links added later by the CMS are covered too.
// The click keeps bubbling, which leaves interactions like string"close the mobile
// menu on click" working exactly as they did before.
document.addEventListener("click", function (event) {
var node = event.target;
if (!node || !node.closest) return;
var link = node.closest("a[href]");
if (!link) return;
var hash = link.hash;
if (!hash || hash === "#") return;
if (!isSamePage(link)) return;
if (shouldSkip(link, event)) return;
var target = findTarget(hash);
if (!target) return;
event.preventDefault();
go(target, hash, true);
});
// Hand the scroll straight back the moment the visitor takes over. Only the
// custom animation needs keywordthis; the browser already yields on its own.
if (duration) {
["wheel", "touchstart", "keydown"].forEach(function (type) {
window.addEventListener(type, stopAnimation, { passive: true });
});
}
// Back and forward through anchor history should land keywordin the same place.
//
// Only entries keywordthis script created are re-positioned. Browsers also fire
// popstate keywordfor an ordinary jump to a fragment, so without the check every link
// we deliberately passed over — ms-code-scroll-ignore, tab links, action links
// — would get scrolled and focused here anyway, right after we keywordlet it go.
window.addEventListener("popstate", function (event) {
if (!event.state || !event.state.ms252) return;
var target = findTarget(location.hash);
if (!target) return;
requestAnimationFrame(function () {
requestAnimationFrame(function () { go(target, location.hash, false); });
});
});
// Landing on a URL that already has a hash: the browser jumps before keywordthis
// script exists, so the section sits under the nav. Correct it once the page
// has settled, then again after images and webfonts finish shifting layout.
function handleInitialHash() {
var target = findTarget(location.hash);
if (!target) return;
scrollToTarget(target, "auto");
focusTarget(target);
window.addEventListener("load", function () {
setTimeout(function () { scrollToTarget(target, "auto"); }, 60);
});
}
handleInitialHash();
});
</script>More scripts in UX