I needed to make sure that the dialog locked the background when activated, and this is what ChatGPT and I came up with. Not only does it lock the background, but it also prevents a layout shift. Just paste it in the JS window of the Etch Dialog component. I suspect maybe this will be an option in the future. I am using this for the search modal.
(function () {
const DIALOG_SELECTOR = 'dialog.etch-dialog';
function getScrollY() {
return window.scrollY || document.documentElement.scrollTop || 0;
}
function getScrollbarWidth() {
return window.innerWidth - document.documentElement.clientWidth;
}
function anyDialogOpen() {
return Array.from(document.querySelectorAll(DIALOG_SELECTOR)).some(
(d) => d && (d.open || d.hasAttribute('open'))
);
}
function getScrollbarWidth() {
// Works on Windows/Linux, returns 0 on overlay-scrollbar systems (macOS)
return window.innerWidth - document.documentElement.clientWidth;
}
function lockScroll() {
const root = document.documentElement;
const body = document.body;
if (root.dataset.etchScrollLock === 'true') return;
const scrollY = window.scrollY || root.scrollTop || 0;
const sbw = getScrollbarWidth();
root.dataset.etchScrollLock = 'true';
body.dataset.etchScrollY = String(scrollY);
root.dataset.etchScrollbarW = String(sbw);
// ✅ Reserve the gutter explicitly
if (sbw > 0) {
root.style.paddingRight = `${sbw}px`;
// Optional: if you have elements that are fixed to the right edge,
// padding on html helps keep layout consistent.
}
// Freeze page
body.style.position = 'fixed';
body.style.top = `-${scrollY}px`;
body.style.left = '0';
body.style.right = '0';
body.style.width = '100%';
}
function unlockScroll() {
const root = document.documentElement;
const body = document.body;
if (root.dataset.etchScrollLock !== 'true') return;
const scrollY = parseInt(body.dataset.etchScrollY || '0', 10) || 0;
// Clear
body.style.position = '';
body.style.top = '';
body.style.left = '';
body.style.right = '';
body.style.width = '';
root.style.paddingRight = '';
delete body.dataset.etchScrollY;
delete root.dataset.etchScrollbarW;
delete root.dataset.etchScrollLock;
window.scrollTo(0, scrollY);
}
function sync() {
if (anyDialogOpen()) lockScroll();
else unlockScroll();
}
function attach(dialog) {
// Close/cancel are reliable for unlock
dialog.addEventListener('close', sync);
dialog.addEventListener('cancel', sync);
// Watch open attribute (locks ASAP)
const mo = new MutationObserver(sync);
mo.observe(dialog, { attributes: true, attributeFilter: ['open'] });
// Initial
sync();
}
document.querySelectorAll(DIALOG_SELECTOR).forEach(attach);
// Catch dialogs added later
const rootObserver = new MutationObserver((mutations) => {
for (const m of mutations) {
for (const n of m.addedNodes) {
if (!(n instanceof Element)) continue;
if (n.matches?.(DIALOG_SELECTOR)) attach(n);
n.querySelectorAll?.(DIALOG_SELECTOR).forEach(attach);
}
}
sync();
});
rootObserver.observe(document.documentElement, { childList: true, subtree: true });
// Extra safety: if something opens on load
window.addEventListener('load', sync);
})();JavaScript
Leave a Reply