C.W.K.
Stream
Lesson 06 of 07 · published

Preview and Confirm — Earn Trust Before You Mutate

~11 min · preview-confirm, ux, trust, overlay

Level 0Extension Curious
0 XP0/54 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"The user's page is their context. The moment ClipDeck reaches in to fill a form, highlight a region, or mutate the DOM, the user deserves to see what's about to happen and back out cleanly. Lesson 6 is the preview-and-confirm overlay pattern that turns extension actions into a small contract instead of a surprise."

Why a Preview at All

For passive actions (read selection, save clip, take screenshot), no preview needed — the user initiated, the result lands in the side panel, nothing on the page changed. For active actions (auto-fill an input, click a button, dismiss a modal, scroll into view), the equation flips:

  • The user can't always tell what the extension is about to do — the click target, the value to paste, the element to scroll to. A preview names it.
  • The user might have set up the page in a specific way (form half-filled, mid-scroll). A preview gives them a moment to confirm or back out before that work is touched.
  • Real-world pages have edge cases extension code can't anticipate. A preview that surfaces the planned action lets the user catch obvious wrongness before commit.

The friction is small (one click or one tap of Enter to confirm). The trust gain is enormous.

The Overlay Pattern

Inject a small overlay into the host page from the content script. The overlay describes the action, highlights the target if any, and offers Confirm / Cancel:

  • Position: floating at the top-right of the viewport, or anchored near the action target. Never modal — the user should still be able to scroll the page if they need more context.
  • Content: one-line summary of the action ("Paste 'service worker eviction' into this input"), a bordered preview of the target element, two buttons.
  • Lifecycle: appears when the SW or popup initiates an action; dismissed on Confirm (action runs), Cancel (action aborts), or 10-second timeout (action aborts by default).

Keyboard Affordances

Enter confirms; Escape cancels. Focus the Confirm button on mount so the keyboard path is obvious. The default behavior should match what 90% of users want — usually Confirm — but the user should never feel ambushed because they hit Enter on a focused button they didn't see.

Target Highlighting

When the action affects a specific element, outline it with a temporary border so the user can see what's about to be touched:

function highlightElement(el) {
  const original = el.style.outline;
  el.style.outline = '2px solid #1a6bd6';
  el.style.outlineOffset = '2px';
  return () => { el.style.outline = original; };
}

Return a cleanup function so confirm/cancel can both unhighlight. The outline is visually loud but doesn't shift layout, which preserves the user's mental model of where things are.

The Z-Index Wars

Real pages have z-index stacks that fight you. Use the absolute max:

overlay.style.zIndex = '2147483647'; // 2^31 - 1, the maximum int

And use position: fixed so the overlay stays on top of the viewport regardless of page scroll. Pages with fixed headers sometimes still occlude; if that happens, render the overlay inside a :popover element (Chrome 114+) which is in its own top-layer and bypasses z-index entirely.

The Shadow Root Wrap

For extra safety against page CSS bleeding into your overlay, attach a shadow root to your overlay container and put your markup + styles inside it:

const host = document.createElement('div');
host.style.cssText = 'position:fixed;top:0;right:0;z-index:2147483647;';
document.body.appendChild(host);
const shadow = host.attachShadow({ mode: 'closed' });
shadow.innerHTML = `<style>...</style><div>...</div>`;

Closed shadow root means page scripts can't querySelector into it. Your overlay is invisible to the page's own analytics, accessibility tools, and event listeners — useful for extension UI that shouldn't be observable.

The Confirmation Promise

Wrap the whole flow in a Promise the caller awaits:

const confirmed = await previewAndConfirm({
  summary: 'Paste "' + clip.text.slice(0, 60) + '" into this input',
  target: focusedElement,
  timeoutMs: 10000,
});
if (confirmed) await fillInput(focusedElement, clip.text);

Three exits: confirmed (true), cancelled (false), timeout (false). The caller branches on the boolean — no event listener dance, no callback hell. The overlay teardown happens before the Promise resolves.

Active mutation earns a preview. Show the action in plain language, outline the target, offer Confirm / Cancel with keyboard affordances, default to Cancel after 10 seconds. Tiny friction, enormous trust gain.
When you can skip preview. If the user invoked an unambiguous action with a specific target they already see (typed Ctrl+Shift+K on selected text → save clip), the preview can be a one-second toast confirmation after the fact instead of a confirmation before. Reserve the heavyweight overlay for actions where the target isn't already obvious from the user's intent.

Code

content.js — full previewAndConfirm helper returning a Promise·javascript
// content.js — previewAndConfirm overlay with shadow root + keyboard nav
function previewAndConfirm({ summary, target, timeoutMs = 10000 }) {
  return new Promise((resolve) => {
    const host = document.createElement("div");
    host.style.cssText = "position:fixed;top:16px;right:16px;z-index:2147483647;";
    document.body.appendChild(host);
    const shadow = host.attachShadow({ mode: "closed" });
    shadow.innerHTML = `
      <style>
        .panel { background: #fff; color: #222; border: 1px solid #1a6bd6; border-radius: 6px;
          padding: 12px 14px; box-shadow: 0 4px 14px rgba(0,0,0,0.15); font-family: system-ui, sans-serif;
          font-size: 13px; min-width: 280px; max-width: 360px; }
        .summary { margin: 0 0 10px; line-height: 1.4; }
        .row { display: flex; gap: 8px; justify-content: flex-end; }
        button { padding: 4px 10px; font: inherit; border-radius: 4px; cursor: pointer; }
        .confirm { background: #1a6bd6; color: #fff; border: 1px solid #1a6bd6; }
        .cancel { background: #fff; color: #1a6bd6; border: 1px solid #1a6bd6; }
      </style>
      <div class="panel" role="alertdialog">
        <p class="summary"></p>
        <div class="row">
          <button class="cancel">Cancel</button>
          <button class="confirm">Confirm</button>
        </div>
      </div>`;
    shadow.querySelector(".summary").textContent = summary;

    let cleanupTarget = () => {};
    if (target) {
      const original = target.style.outline;
      target.style.outline = "2px solid #1a6bd6";
      target.style.outlineOffset = "2px";
      cleanupTarget = () => { target.style.outline = original; };
    }

    const teardown = (result) => {
      cleanupTarget();
      host.remove();
      clearTimeout(timer);
      window.removeEventListener("keydown", onKey, true);
      resolve(result);
    };
    const timer = setTimeout(() => teardown(false), timeoutMs);
    const onKey = (e) => {
      if (e.key === "Enter") { e.preventDefault(); teardown(true); }
      if (e.key === "Escape") { e.preventDefault(); teardown(false); }
    };
    window.addEventListener("keydown", onKey, true);
    shadow.querySelector(".confirm").addEventListener("click", () => teardown(true));
    shadow.querySelector(".cancel").addEventListener("click", () => teardown(false));
    shadow.querySelector(".confirm").focus();
  });
}
content.js — preview, then fillInput on confirm·javascript
// content.js — example usage: paste a clip into the focused input with preview
async function pasteClipWithPreview(clip) {
  const target = document.activeElement;
  if (!target || target === document.body) return;
  const ok = await previewAndConfirm({
    summary: `Paste \u201C${clip.text.slice(0, 60)}\u2026\u201D into this input`,
    target,
  });
  if (!ok) return;
  // CD_TOOLS.fillInput from Lesson 5
  await CD_TOOLS.fillInput({
    selector: generateSelector(target),
    value: clip.text,
  });
}

External links

Exercise

Add the first code block (previewAndConfirm) to clipdeck/content.js. From the content script's DevTools console on any page, call previewAndConfirm({ summary: 'Test action on this body', target: document.body }). The overlay appears in the top-right; document.body gets a blue outline. Press Enter — Promise resolves true. Try again, press Escape — resolves false. Try with timeoutMs: 3000 and don't interact — after 3 seconds Promise resolves false. Confirm the overlay's CSS doesn't bleed onto the page (the shadow root prevents that) and that aggressive z-index keeps it on top of anything the page renders.
Hint
If the overlay appears but doesn't take keyboard focus, the .confirm button selector might be missing — check that shadow.querySelector(".confirm") returns a non-null element AND that the focus() call runs after the shadow root has rendered. If pressing Enter in an input on the page accidentally triggers Confirm, the keydown listener's true capture phase is doing exactly what it's supposed to — but you may want to limit it to ignore events whose target is outside the overlay. For real pages with persistent overlays (web apps that already use fixed top-right notifications), test that they don't visually overlap; if they do, anchor your overlay to the target element instead of a fixed corner.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.