A form input mask that avoids layout jumps while you type

Yo folks, I’m tuning a checkout form and I keep noticing tiny layout shifts when an input mask/validation message kicks in, especially on mobile where the placeholder width changes and the range slider below gets pushed.

// Keeps the input's rendered width stable by reserving space for the mask.
// Uses a hidden "mirror" span to measure text, then writes width once per frame.
export function stableMaskedInput(input, { mask = v => v, minCh = 1 } = {}) {
  const mirror = document.createElement("span");
  mirror.setAttribute("aria-hidden", "true");
  mirror.style.cssText = `
    position: absolute;
    top: -9999px;
    left: -9999px;
    white-space: pre;
    visibility: hidden;
  `;
  document.body.appendChild(mirror);

  let raf = 0;
  const syncFont = () => {
    const cs = getComputedStyle(input);
    mirror.style.font = cs.font;
    mirror.style.letterSpacing = cs.letterSpacing;
    mirror.style.textTransform = cs.textTransform;
  };

  const reserve = () => {
    cancelAnimationFrame(raf);
    raf = requestAnimationFrame(() => {
      syncFont();
      const raw = input.value;
      const masked = mask(raw);

      // Prefer current value, but fall back to placeholder so empty state doesn't shrink.
      const text = masked || input.placeholder || "";
      mirror.textContent = text.padEnd(minCh, " ");

      // +2ch-ish padding so caret doesn't feel cramped.
      const px = mirror.getBoundingClientRect().width;
      input.style.inlineSize = `calc(${Math.ceil(px)}px + 1.5ch)`;
    });
  };

  input.addEventListener("input", reserve, { passive: true });
  input.addEventListener("blur", reserve, { passive: true });
  window.addEventListener("resize", reserve, { passive: true });
  reserve();

  return () => {
    cancelAnimationFrame(raf);
    mirror.remove();
    input.removeEventListener("input", reserve);
    input.removeEventListener("blur", reserve);
    window.removeEventListener("resize", reserve);
  };
}

Neat part is it batches DOM reads/writes so the render pipeline stays calm, and it stops the validation + placeholder swap from nudging nearby form rows (my range control stays put).