
Controls Lenis scrolling using simple data attributes, allowing you to lock, unlock, or toggle scroll based on clicks or element visibility. It also handles the scrollbar gap automatically to prevent layout shift when the scrollbar is hidden.
Place the scripts before the body tag of your project. If you have added them before for another setup, skip this step.
<script src="https://cdn.jsdelivr.net/npm/tua-body-scroll-lock"></script>Paste the script through Elementor → Custom Code and set it to load after the closing body tag.
document.addEventListener("DOMContentLoaded", () => {
if (document.body.classList.contains("elementor-editor-active")) return;
const lenis = window.ouLenis;
const bodyLock = window.bodyScrollLock || window.tuaBodyScrollLock;
if (!lenis || !bodyLock) return;
const gap = "data-lenis-scrollbar-gap";
const visibleSelector = "[data-lenis-visible]";
let locked = false;
let manualOverride = false;
const visibleEls = document.querySelectorAll(visibleSelector);
const shouldUseGap = el =>
!el || el.getAttribute(gap) !== "false";
const setScroll = (shouldLock, el, manual = false) => {
if (manual) manualOverride = !shouldLock;
if (locked === shouldLock) return;
if (shouldLock) {
bodyLock.lock(el || document.body, {
withPaddingRight: shouldUseGap(el)
});
lenis.stop();
locked = true;
return;
}
bodyLock.clearBodyLocks();
lenis.start();
locked = false;
};
const updateVisible = el => {
if (manualOverride) return;
const hidden = getComputedStyle(el).display === "none";
const rect = el.getBoundingClientRect();
const inView = rect.top < window.innerHeight && rect.bottom > 0;
setScroll(!hidden && inView, el);
};
document.addEventListener("click", e => {
const start = e.target.closest("[data-lenis-start]");
const stop = e.target.closest("[data-lenis-stop]");
const toggle = e.target.closest("[data-lenis-toggle]");
if (start) {
setScroll(false, start, true);
return;
}
if (stop) {
manualOverride = false;
setScroll(true, stop, true);
return;
}
if (toggle) {
setScroll(!locked, toggle, true);
return;
}
});
if (!visibleEls.length) return;
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (manualOverride) return;
const el = entry.target;
const hidden = getComputedStyle(el).display === "none";
setScroll(entry.isIntersecting && !hidden, el);
});
});
visibleEls.forEach(el => observer.observe(el));
new MutationObserver(() => {
manualOverride = false;
visibleEls.forEach(updateVisible);
}).observe(document.body, {
attributes: true,
subtree: true,
attributeFilter: ["style", "class", gap]
});
});Some solutions only work on the live site. Always publish and test after each change, as results may not appear in the editor.
Use data-lenis-stop="true" on the element that should pause page scrolling when clicked. This is usually best for modal open buttons, menu triggers, popup buttons, or any interaction that opens an overlay where the background page should not move.
Use data-lenis-start="true" on the element that should resume page scrolling when clicked. This is usually used on close buttons, modal overlays, menu close icons, or any element that exits the locked-scroll state.
Use data-lenis-toggle="true" when the same element controls both opening and closing, such as a hamburger menu button or custom toggle button. Each click switches page scrolling between locked and unlocked state.
Use data-lenis-visible="true" on the element that should control scroll locking based on visibility. When this element is visible in the viewport, scrolling pauses. When it leaves the viewport or becomes hidden with display: none, scrolling resumes. This is best for modals, overlays, panels, or any dynamic element.
Use data-lenis-scrollbar-gap="false" only when you do not want the script to reserve space for the browser scrollbar. By default, the gap fix is enabled to prevent the layout from shifting sideways when the scrollbar disappears. This attribute can be added to any element that triggers or controls the scroll state.