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