Open demo

CSS code

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <symbol id="np-i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6.5 6.5l11 11M17.5 6.5l-11 11"/></symbol>
  <symbol id="np-i-mail" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3.5 6.5L12 13l8.5-6.5"/></symbol>
  <symbol id="np-i-tick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.5l4.5 4.5L19 7.5"/></symbol>
  <symbol id="np-i-vase" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M9 3h6M10 3v3c-3 1.5-5 4.5-5 8a7 7 0 0 0 14 0c0-3.5-2-6.5-5-8V3M6 14h12"/></symbol>
</svg>

<div>
  <span class="lbl">Centered popup</span>
  <div class="demo-row"><button class="np-trigger" type="button" data-open="np-dispatch" aria-haspopup="dialog"><svg aria-hidden="true"><use href="#np-i-mail"/></svg>Open the popup</button><p>Modal: Esc, the close button, Not now or a backdrop click closes it.</p></div>
  <div class="stage stage--center" data-preview="np-dispatch" aria-hidden="true" inert></div>
</div>

<div>
  <span class="lbl">Corner slide-in</span>
  <div class="demo-row"><button class="np-trigger" type="button" data-open="np-kiln" aria-haspopup="dialog"><svg aria-hidden="true"><use href="#np-i-mail"/></svg>Show the slide-in</button><p>Non-modal: the page stays usable. Esc or the close button dismisses it.</p></div>
  <div class="stage stage--corner" data-preview="np-kiln" aria-hidden="true" inert></div>
</div>

<div>
  <span class="lbl">Bottom bar</span>
  <div class="demo-row"><button class="np-trigger" type="button" data-open="np-halden" aria-haspopup="dialog"><svg aria-hidden="true"><use href="#np-i-mail"/></svg>Show the bar</button><p>Non-modal, full width, for a quieter ask.</p></div>
  <div class="stage stage--bar" data-preview="np-halden" aria-hidden="true" inert></div>
</div>

<!-- 1. Centered popup (showModal) -->
<dialog class="np np-modal" id="np-dispatch" data-modal closedby="any" aria-labelledby="np-dispatch-title">
  <div class="np-split">
    <div class="np-art" aria-hidden="true"><b>Northline</b><span>Read by 52,000 walkers</span></div>
    <div class="np-main">
      <button class="np-x" type="button" data-close aria-label="Close"><svg aria-hidden="true"><use href="#np-i-x"/></svg></button>
      <form class="np-form" action="#" method="post" data-nf="np-dispatch-done">
        <p class="np-kicker">Free, every Sunday</p>
        <h2 class="np-title" id="np-dispatch-title">Get the Sunday Dispatch</h2>
        <p class="np-text">Three routes worth the drive, one gear test we paid for ourselves, and a long read for the evening.</p>
        <div class="np-field">
          <label class="np-label" for="np-dispatch-email">Email address</label>
          <div class="np-row">
            <input class="np-input" id="np-dispatch-email" name="email" type="email" autocomplete="email" required autofocus aria-describedby="np-dispatch-err np-dispatch-hint" data-err="np-dispatch-err"
              data-missing="Enter your email address to subscribe." data-bad="Enter an email address with an @ and a domain, like [email protected].">
            <button class="np-btn" type="submit">Subscribe</button>
          </div>
          <p class="np-err" id="np-dispatch-err"></p>
        </div>
        <div class="np-foot">
          <p class="np-hint" id="np-dispatch-hint">One email a week. <a href="#privacy">Privacy notice</a></p>
          <button class="np-plain" type="button" data-close>Not now</button>
        </div>
      </form>
      <div class="np-done" id="np-dispatch-done" tabindex="-1" hidden>
        <span class="np-done-ico" aria-hidden="true"><svg><use href="#np-i-tick"/></svg></span>
        <h2 class="np-title">Check your inbox</h2>
        <p class="np-text">We sent a confirmation link to <b data-email>you</b>. The next Dispatch goes out on Sunday at 7am.</p>
        <div class="np-foot"><button class="np-btn" type="button" data-close>Back to the article</button></div>
      </div>
    </div>
  </div>
</dialog>

<!-- 2. Corner slide-in (show) -->
<dialog class="np np-corner" id="np-kiln" aria-labelledby="np-kiln-title">
  <div class="np-corner-in">
    <button class="np-x" type="button" data-close aria-label="Close"><svg aria-hidden="true"><use href="#np-i-x"/></svg></button>
    <div class="np-corner-head">
      <span class="np-badge" aria-hidden="true"><svg><use href="#np-i-vase"/></svg></span>
      <div><h2 class="np-title" id="np-kiln-title">Enjoying part 2?</h2>
        <p class="np-text">Get the other four parts of our glazing series by email as each one is published.</p></div>
    </div>
    <form class="np-form" action="#" method="post" data-nf="np-kiln-done">
      <label class="np-label" for="np-kiln-email">Email address</label>
      <div class="np-row">
        <input class="np-input" id="np-kiln-email" name="email" type="email" autocomplete="email" required aria-describedby="np-kiln-err np-kiln-hint" data-err="np-kiln-err"
          data-missing="Enter your email address." data-bad="Enter an email address with an @ and a domain, like [email protected].">
        <button class="np-btn" type="submit">Send me the series</button>
      </div>
      <p class="np-err" id="np-kiln-err"></p>
      <p class="np-hint" id="np-kiln-hint">Five emails from Kiln & Clay, then nothing unless you ask.</p>
    </form>
    <div class="np-done" id="np-kiln-done" tabindex="-1" hidden>
      <p class="np-text"><b>Done.</b> Part 3 will reach <b data-email>you</b> on 2 October, once you confirm the email we just sent.</p>
    </div>
  </div>
</dialog>

<!-- 3. Bottom bar (show) -->
<dialog class="np np-bar" id="np-halden" aria-labelledby="np-halden-title">
  <div class="np-bar-in">
    <button class="np-x" type="button" data-close aria-label="Close"><svg aria-hidden="true"><use href="#np-i-x"/></svg></button>
    <div class="np-bar-copy"><h2 class="np-title" id="np-halden-title">The Thursday deals digest</h2>
      <p class="np-text">Real discounts on tents, boots and stoves from Halden Outdoor. Weekly, never daily.</p></div>
    <form class="np-form" action="#" method="post" data-nf="np-halden-done">
      <label class="np-label" for="np-halden-email">Email address</label>
      <div class="np-row">
        <input class="np-input" id="np-halden-email" name="email" type="email" autocomplete="email" required placeholder="Email address" aria-describedby="np-halden-err" data-err="np-halden-err"
          data-missing="Enter your email address." data-bad="Enter an email address with an @ and a domain, like [email protected].">
        <button class="np-btn" type="submit">Get the digest</button>
      </div>
      <p class="np-err" id="np-halden-err"></p>
    </form>
    <div class="np-done" id="np-halden-done" tabindex="-1" hidden>
      <span class="np-done-ico" aria-hidden="true"><svg><use href="#np-i-tick"/></svg></span>
      <p class="np-text">Confirm the email we sent to <b data-email>you</b> and Thursday's digest is yours.</p>
    </div>
  </div>
</dialog>
CSS
/* Newsletter popups built on the native <dialog>.
   - The centered popup opens with showModal(): focus moves into it, the page behind is inert, Esc closes it, and
     a click on the backdrop closes it too. On close, focus goes back to the button that opened it.
   - The corner slide-in and the bottom bar open with show(): the page stays usable, Esc and the close button dismiss them.
   - No dark patterns: a visible close button, a plain "Not now", no pre-ticked boxes, no guilt-trip copy, and nothing
     opens on page load. Open them from a button, or after a scroll depth you choose, and remember the dismissal. */
html:has(.np-modal[open]){overflow:hidden}
.np{--np-ink:#1a1d24;--np-muted:#535866;--np-line:#7b808d;--np-accent:#be123c;--np-accent-dark:#9f1239;--np-ring:rgba(190,18,60,.22);--np-error:#b42318;--np-ok:#15803d;
  padding:0;border:0;background:#fff;color:var(--np-ink);font-size:.92rem;line-height:1.5;box-shadow:0 0 0 1px rgba(26,29,36,.06),0 30px 70px -24px rgba(26,29,36,.55)}
.np *,.np *::before,.np *::after{box-sizing:border-box}
.np a{color:var(--np-accent-dark);font-weight:600;text-underline-offset:2px}
.np a:focus-visible{outline:2px solid var(--np-accent);outline-offset:2px;border-radius:3px}
.np-trigger{display:inline-flex;align-items:center;gap:8px;font:inherit;font-size:.9rem;font-weight:650;color:#fff;background:#be123c;border:0;border-radius:10px;padding:10px 16px;cursor:pointer;
  transition:background-color .15s}
.np-trigger:hover{background:#9f1239}
.np-trigger:focus-visible{outline:2px solid #be123c;outline-offset:3px}
.np-trigger svg{width:18px;height:18px}
.np-x{position:absolute;top:10px;right:10px;z-index:2;display:grid;place-items:center;width:36px;height:36px;padding:0;border:0;border-radius:10px;background:rgba(255,255,255,.9);
  color:var(--np-muted);cursor:pointer;transition:background-color .15s,color .15s}
.np-x:hover{background:#eceef2;color:var(--np-ink)}
.np-x:focus-visible{outline:2px solid var(--np-accent);outline-offset:1px}
.np-x svg{width:20px;height:20px}
.np-kicker{font-size:.72rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--np-accent-dark)}
.np-title{font-size:1.4rem;font-weight:750;letter-spacing:-.02em;line-height:1.2;margin:4px 0 6px}
.np-text{color:var(--np-muted)}
.np-label{display:block;font-size:.84rem;font-weight:650;margin-bottom:6px}
.np-row{display:flex;gap:8px}
.np-input{display:block;flex:1 1 0;width:100%;min-width:0;font:inherit;color:var(--np-ink);background:#fff;border:1.5px solid var(--np-line);border-radius:10px;padding:10px 13px;
  transition:border-color .15s,box-shadow .15s}
.np-input:focus{outline:none;border-color:var(--np-accent);box-shadow:0 0 0 4px var(--np-ring)}
.np-input.is-invalid{border-color:var(--np-error)}
.np-btn{flex:none;font:inherit;font-weight:650;color:#fff;background:var(--np-accent);border:0;border-radius:10px;padding:10px 18px;cursor:pointer;transition:background-color .15s}
.np-btn:hover{background:var(--np-accent-dark)}
.np-btn:focus-visible{outline:2px solid var(--np-accent);outline-offset:3px}
.np-plain{font:inherit;font-size:.86rem;font-weight:600;color:var(--np-muted);background:none;border:0;border-radius:6px;padding:4px 6px;margin-left:-6px;cursor:pointer;text-decoration:underline;text-underline-offset:2px}
.np-plain:hover{color:var(--np-ink)}
.np-plain:focus-visible{outline:2px solid var(--np-accent);outline-offset:1px}
.np-err{display:flex;gap:7px;align-items:flex-start;font-size:.82rem;font-weight:600;color:var(--np-error);margin-top:7px}
.np-err:empty{display:none}
.np-err::before{content:"!";flex:none;display:grid;place-items:center;width:16px;height:16px;margin-top:2px;border-radius:50%;background:var(--np-error);color:#fff;font-size:.7rem;font-weight:800}
.np-hint{font-size:.78rem;color:var(--np-muted);margin-top:8px}
.np-done{outline:none}
.np-done[hidden],.np-form[hidden]{display:none}
.np-done:focus-visible{outline:2px solid var(--np-accent);outline-offset:4px;border-radius:10px}
.np-done-ico{display:grid;place-items:center;width:44px;height:44px;border-radius:50%;background:#dcfce7;color:var(--np-ok);margin-bottom:10px}
.np-done-ico svg{width:22px;height:22px}
.np-done b[data-email]{overflow-wrap:anywhere}

/* 1. Centered popup: illustration panel beside the form */
.np-modal{position:fixed;inset:0;width:min(680px,calc(100% - 32px));max-height:calc(100dvh - 32px);margin:auto;border-radius:20px;overflow:auto;overscroll-behavior:contain}
.np-modal[open]{animation:np-pop .22s cubic-bezier(.2,.8,.2,1)}
.np-modal::backdrop{background:rgba(17,20,28,.55)}
.np-split{display:grid;grid-template-columns:240px 1fr}
.np-art{position:relative;min-height:100%;padding:22px;color:#fff;display:flex;flex-direction:column;justify-content:flex-end;
  background:radial-gradient(circle at 70% 26%,#ffe4b5 0 22px,rgba(255,228,181,.25) 23px 36px,transparent 37px),
    linear-gradient(160deg,transparent 52%,#3f1d2e 52%) ,linear-gradient(200deg,transparent 60%,#5b2140 60%),linear-gradient(180deg,#f59e7a,#be4a6a 60%,#7a1f45)}
.np-art b{font-size:1.05rem;line-height:1.25}
.np-art span{font-size:.8rem;opacity:.9}
.np-main{position:relative;padding:34px 30px 26px}
.np-field{margin-top:16px}
.np-foot{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:6px 12px;margin-top:14px}
.np-foot .np-hint{margin:0}

/* 2. Corner slide-in: non-modal, bottom right */
.np-corner{position:fixed;inset:auto 20px 20px auto;width:min(360px,calc(100% - 24px));margin:0;border-radius:16px}
.np-corner[open]{animation:np-slide .3s cubic-bezier(.2,.8,.2,1)}
.np-corner-in{position:relative;padding:20px 20px 18px}
.np-corner-head{display:flex;gap:12px;align-items:flex-start;padding-right:30px}
.np-badge{flex:none;display:grid;place-items:center;width:40px;height:40px;border-radius:12px;background:#ffe4e6;color:var(--np-accent-dark)}
.np-badge svg{width:22px;height:22px}
.np-corner .np-title{font-size:1.05rem;margin:0 0 2px}
.np-corner .np-text{font-size:.86rem}
.np-corner .np-form{margin-top:14px}
.np-corner .np-done{margin-top:12px}
.np-corner .np-row{flex-direction:column}
.np-corner .np-x{top:8px;right:8px}

/* 3. Bottom bar: non-modal, full width */
.np-bar{position:fixed;inset:auto 0 0 0;width:100%;max-width:none;margin:0;border-radius:16px 16px 0 0;border-top:3px solid var(--np-accent)}
.np-bar[open]{animation:np-rise .3s cubic-bezier(.2,.8,.2,1)}
.np-bar-in{position:relative;display:flex;flex-wrap:wrap;align-items:center;gap:12px 28px;max-width:1040px;margin:0 auto;padding:18px 60px 18px 24px}
.np-bar-copy{flex:1 1 280px;min-width:0}
.np-bar .np-title{font-size:1.05rem;margin:0}
.np-bar .np-text{font-size:.86rem}
.np-bar .np-form,.np-bar .np-done{flex:1 1 340px;min-width:0}
.np-bar .np-label{position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}
.np-bar .np-x{top:50%;right:12px;margin-top:-18px}
.np-bar .np-done:not([hidden]){display:flex;gap:12px;align-items:center}
.np-bar .np-done .np-done-ico{margin:0;flex:none}

@keyframes np-pop{from{opacity:0;transform:translateY(10px) scale(.97)}}
@keyframes np-slide{from{opacity:0;transform:translateX(24px)}}
@keyframes np-rise{from{transform:translateY(100%)}}
@media (max-width:600px){.np-split{grid-template-columns:1fr}.np-art{min-height:120px;padding:18px}.np-main{padding:26px 20px 22px}
  .np-corner{inset:auto 12px 12px 12px;width:auto}.np-bar-in{padding:16px 56px 16px 18px}.np-bar .np-x{top:12px;margin-top:0}}
@media (max-width:420px){.np-row{flex-direction:column}}
@media (prefers-reduced-motion:reduce){.np-modal[open],.np-corner[open],.np-bar[open]{animation:none}.np-trigger,.np-x,.np-input,.np-btn{transition:none}}
JavaScript
// Opening and closing: buttons with data-open="id" open a dialog; showModal() when it has data-modal, show() otherwise.
// Esc closes both kinds, a backdrop click closes the modal one, and focus returns to the button that opened it.
// The forms are a front-end demo: nothing is sent. Errors are written into the element each input points to with
// aria-describedby; a valid submit swaps in the success state. Replace that branch with your fetch() call.
(function () {
  document.querySelectorAll('[data-open]').forEach(function (btn) {
    btn.addEventListener('click', function () {
      var dialog = document.getElementById(btn.getAttribute('data-open'));
      if (!dialog || dialog.open) return;
      dialog.opener = btn;
      if (dialog.hasAttribute('data-modal')) dialog.showModal(); else dialog.show();
      if (!dialog.hasAttribute('data-modal')) { var first = dialog.querySelector('input,button:not([data-close])'); if (first) first.focus(); }
    });
  });
  document.querySelectorAll('dialog.np').forEach(function (dialog) {
    dialog.querySelectorAll('[data-close]').forEach(function (b) { b.addEventListener('click', function () { dialog.close(); }); });
    dialog.addEventListener('close', function () { if (dialog.opener) dialog.opener.focus(); });
    // Esc for non-modal dialogs (modal ones get it from the browser)
    dialog.addEventListener('keydown', function (e) { if (e.key === 'Escape' && !dialog.matches(':modal')) { e.preventDefault(); dialog.close(); } });
    // light dismiss: a click that lands on the dialog element itself is a click on the backdrop
    dialog.addEventListener('click', function (e) { if (e.target === dialog && dialog.matches(':modal')) dialog.close(); });
  });

  function check(el) {
    var msg = el.checkValidity() ? '' : el.getAttribute(el.validity.valueMissing ? 'data-missing' : 'data-bad');
    var err = document.getElementById(el.getAttribute('data-err'));
    if (err) err.textContent = msg || '';
    el.classList.toggle('is-invalid', !!msg);
    if (msg) el.setAttribute('aria-invalid', 'true'); else el.removeAttribute('aria-invalid');
    return !msg;
  }
  document.querySelectorAll('form[data-nf]').forEach(function (form) {
    var done = document.getElementById(form.getAttribute('data-nf'));
    var email = form.querySelector('input[type=email]');
    form.noValidate = true;
    form.addEventListener('submit', function (e) {
      e.preventDefault();
      if (!check(email)) { email.focus(); return; }
      done.querySelectorAll('[data-email]').forEach(function (n) { n.textContent = email.value; });
      form.hidden = true; done.hidden = false; done.focus();
    });
    email.addEventListener('input', function () { if (email.classList.contains('is-invalid')) check(email); });
  });
})();

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <symbol id="np-i-mail" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3.5 6.5L12 13l8.5-6.5"/></symbol>
  <symbol id="np-i-tick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.5l4.5 4.5L19 7.5"/></symbol>
  <symbol id="np-i-vase" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M9 3h6M10 3v3c-3 1.5-5 4.5-5 8a7 7 0 0 0 14 0c0-3.5-2-6.5-5-8V3M6 14h12"/></symbol>
</svg>

<div>
  <span class="lbl">Centered popup</span>
  <div class="demo-row"><button class="btn np-trigger" type="button" data-open="np-dispatch" aria-haspopup="dialog"><svg aria-hidden="true"><use href="#np-i-mail"/></svg>Open the popup</button><p>Modal: Esc, the close button, Not now or a backdrop click closes it.</p></div>
  <div class="stage stage--center" data-preview="np-dispatch" aria-hidden="true" inert></div>
</div>

<div>
  <span class="lbl">Corner slide-in</span>
  <div class="demo-row"><button class="btn np-trigger" type="button" data-open="np-kiln" aria-haspopup="dialog"><svg aria-hidden="true"><use href="#np-i-mail"/></svg>Show the slide-in</button><p>Non-modal: the page stays usable. Esc or the close button dismisses it.</p></div>
  <div class="stage stage--corner" data-preview="np-kiln" aria-hidden="true" inert></div>
</div>

<div>
  <span class="lbl">Bottom bar</span>
  <div class="demo-row"><button class="btn np-trigger" type="button" data-open="np-halden" aria-haspopup="dialog"><svg aria-hidden="true"><use href="#np-i-mail"/></svg>Show the bar</button><p>Non-modal, full width, for a quieter ask.</p></div>
  <div class="stage stage--bar" data-preview="np-halden" aria-hidden="true" inert></div>
</div>

<!-- 1. Centered popup (showModal) -->
<dialog class="np np-modal" id="np-dispatch" data-modal closedby="any" aria-labelledby="np-dispatch-title">
  <div class="np-split">
    <div class="np-art" aria-hidden="true"><b>Northline</b><span>Read by 52,000 walkers</span></div>
    <div class="np-main">
      <button class="btn-close" type="button" data-close aria-label="Close"></button>
      <form action="#" method="post" data-nf="np-dispatch-done">
        <p class="np-kicker">Free, every Sunday</p>
        <h2 class="np-title" id="np-dispatch-title">Get the Sunday Dispatch</h2>
        <p class="np-text">Three routes worth the drive, one gear test we paid for ourselves, and a long read for the evening.</p>
        <div class="np-field">
          <label class="form-label" for="np-dispatch-email">Email address</label>
          <div class="d-flex gap-2 np-row">
            <input class="form-control" id="np-dispatch-email" name="email" type="email" autocomplete="email" required autofocus aria-describedby="np-dispatch-err np-dispatch-hint" data-err="np-dispatch-err"
              data-missing="Enter your email address to subscribe." data-bad="Enter an email address with an @ and a domain, like [email protected].">
            <button class="btn np-btn" type="submit">Subscribe</button>
          </div>
          <p class="np-err" id="np-dispatch-err"></p>
        </div>
        <div class="np-foot">
          <p class="form-text" id="np-dispatch-hint">One email a week. <a href="#privacy">Privacy notice</a></p>
          <button class="btn np-plain" type="button" data-close>Not now</button>
        </div>
      </form>
      <div class="np-done" id="np-dispatch-done" tabindex="-1" hidden>
        <span class="np-done-ico" aria-hidden="true"><svg><use href="#np-i-tick"/></svg></span>
        <h2 class="np-title">Check your inbox</h2>
        <p class="np-text">We sent a confirmation link to <b data-email>you</b>. The next Dispatch goes out on Sunday at 7am.</p>
        <div class="np-foot"><button class="btn np-btn" type="button" data-close>Back to the article</button></div>
      </div>
    </div>
  </div>
</dialog>

<!-- 2. Corner slide-in (show) -->
<dialog class="np np-corner" id="np-kiln" aria-labelledby="np-kiln-title">
  <div class="np-corner-in">
    <button class="btn-close" type="button" data-close aria-label="Close"></button>
    <div class="np-corner-head">
      <span class="np-badge" aria-hidden="true"><svg><use href="#np-i-vase"/></svg></span>
      <div><h2 class="np-title" id="np-kiln-title">Enjoying part 2?</h2>
        <p class="np-text">Get the other four parts of our glazing series by email as each one is published.</p></div>
    </div>
    <form action="#" method="post" data-nf="np-kiln-done">
      <label class="form-label" for="np-kiln-email">Email address</label>
      <div class="d-flex flex-column gap-2">
        <input class="form-control" id="np-kiln-email" name="email" type="email" autocomplete="email" required aria-describedby="np-kiln-err np-kiln-hint" data-err="np-kiln-err"
          data-missing="Enter your email address." data-bad="Enter an email address with an @ and a domain, like [email protected].">
        <button class="btn np-btn" type="submit">Send me the series</button>
      </div>
      <p class="np-err" id="np-kiln-err"></p>
      <p class="form-text" id="np-kiln-hint">Five emails from Kiln & Clay, then nothing unless you ask.</p>
    </form>
    <div class="np-done" id="np-kiln-done" tabindex="-1" hidden>
      <p class="np-text"><b>Done.</b> Part 3 will reach <b data-email>you</b> on 2 October, once you confirm the email we just sent.</p>
    </div>
  </div>
</dialog>

<!-- 3. Bottom bar (show) -->
<dialog class="np np-bar" id="np-halden" aria-labelledby="np-halden-title">
  <div class="np-bar-in">
    <button class="btn-close" type="button" data-close aria-label="Close"></button>
    <div class="np-bar-copy"><h2 class="np-title" id="np-halden-title">The Thursday deals digest</h2>
      <p class="np-text">Real discounts on tents, boots and stoves from Halden Outdoor. Weekly, never daily.</p></div>
    <form action="#" method="post" data-nf="np-halden-done">
      <label class="visually-hidden" for="np-halden-email">Email address</label>
      <div class="d-flex gap-2 np-row">
        <input class="form-control" id="np-halden-email" name="email" type="email" autocomplete="email" required placeholder="Email address" aria-describedby="np-halden-err" data-err="np-halden-err"
          data-missing="Enter your email address." data-bad="Enter an email address with an @ and a domain, like [email protected].">
        <button class="btn np-btn" type="submit">Get the digest</button>
      </div>
      <p class="np-err" id="np-halden-err"></p>
    </form>
    <div class="np-done" id="np-halden-done" tabindex="-1" hidden>
      <span class="np-done-ico" aria-hidden="true"><svg><use href="#np-i-tick"/></svg></span>
      <p class="np-text">Confirm the email we sent to <b data-email>you</b> and Thursday's digest is yours.</p>
    </div>
  </div>
</dialog>
CSS
/* Bootstrap .btn, .btn-close, .form-label, .form-control, .form-text and .visually-hidden inside the native <dialog>
   (no Bootstrap JavaScript). showModal() for the popup, show() for the slide-in and the bar. */
html:has(.np-modal[open]){overflow:hidden}
.np{--np-ink:#1a1d24;--np-muted:#535866;--np-line:#7b808d;--np-accent:#be123c;--np-accent-dark:#9f1239;--np-ring:rgba(190,18,60,.22);--np-error:#b42318;--np-ok:#15803d;
  padding:0;border:0;background:#fff;color:var(--np-ink);font-size:.92rem;line-height:1.5;box-shadow:0 0 0 1px rgba(26,29,36,.06),0 30px 70px -24px rgba(26,29,36,.55)}
.np p,.np h2{margin:0}
.np a{color:var(--np-accent-dark);font-weight:600;text-underline-offset:2px}
.np a:focus-visible{outline:2px solid var(--np-accent);outline-offset:2px;border-radius:3px}
.np-trigger{--bs-btn-color:#fff;--bs-btn-bg:#be123c;--bs-btn-border-color:#be123c;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#9f1239;--bs-btn-hover-border-color:#9f1239;
  --bs-btn-active-color:#fff;--bs-btn-active-bg:#9f1239;--bs-btn-active-border-color:#9f1239;--bs-btn-focus-box-shadow:none;--bs-btn-font-size:.9rem;--bs-btn-font-weight:650;
  --bs-btn-padding-x:16px;--bs-btn-padding-y:10px;--bs-btn-border-width:0;--bs-btn-border-radius:10px;--bs-btn-line-height:1.5;display:inline-flex;align-items:center;gap:8px}
.np-trigger:focus-visible{outline:2px solid #be123c;outline-offset:3px}
.np-trigger svg{width:18px;height:18px}
.np .btn-close{--bs-btn-close-opacity:.75;--bs-btn-close-hover-opacity:1;--bs-btn-close-focus-shadow:none;--bs-btn-close-focus-opacity:1;position:absolute;top:10px;right:10px;z-index:2;
  width:20px;height:20px;padding:8px;border-radius:10px;background-size:14px;background-color:rgba(255,255,255,.9)}
.np .btn-close:hover{background-color:#eceef2}
.np .btn-close:focus-visible{outline:2px solid var(--np-accent);outline-offset:1px}
.np-kicker{font-size:.72rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--np-accent-dark)}
.np .np-title{font-size:1.4rem;font-weight:750;letter-spacing:-.02em;line-height:1.2;margin:4px 0 6px}
.np-text{color:var(--np-muted)}
.np .form-label{display:block;font-size:.84rem;font-weight:650;margin-bottom:6px}
.np .form-control{flex:1 1 0;min-width:0;font-size:inherit;line-height:inherit;color:var(--np-ink);border:1.5px solid var(--np-line);border-radius:10px;padding:10px 13px;transition:border-color .15s,box-shadow .15s}
.np .form-control::placeholder{color:#626774}
.np .form-control:focus{border-color:var(--np-accent);box-shadow:0 0 0 4px var(--np-ring)}
.np .form-control.is-invalid{border-color:var(--np-error);background-image:none;padding-right:13px}
.np .np-btn{--bs-btn-color:#fff;--bs-btn-bg:var(--np-accent);--bs-btn-border-color:var(--np-accent);--bs-btn-hover-color:#fff;--bs-btn-hover-bg:var(--np-accent-dark);
  --bs-btn-hover-border-color:var(--np-accent-dark);--bs-btn-active-color:#fff;--bs-btn-active-bg:var(--np-accent-dark);--bs-btn-active-border-color:var(--np-accent-dark);
  --bs-btn-focus-box-shadow:none;--bs-btn-font-weight:650;--bs-btn-font-size:.92rem;--bs-btn-line-height:1.5;--bs-btn-padding-x:18px;--bs-btn-padding-y:10px;
  --bs-btn-border-width:0;--bs-btn-border-radius:10px;flex:none;transition:background-color .15s}
.np .np-btn:focus-visible{outline:2px solid var(--np-accent);outline-offset:3px}
.np .np-plain{--bs-btn-color:var(--np-muted);--bs-btn-hover-color:var(--np-ink);--bs-btn-active-color:var(--np-ink);--bs-btn-font-size:.86rem;--bs-btn-font-weight:600;
  --bs-btn-padding-x:6px;--bs-btn-padding-y:4px;--bs-btn-border-width:0;--bs-btn-border-radius:6px;--bs-btn-focus-box-shadow:none;margin-left:-6px;text-decoration:underline;text-underline-offset:2px}
.np .np-plain:focus-visible{outline:2px solid var(--np-accent);outline-offset:1px}
.np .np-err{display:flex;gap:7px;align-items:flex-start;font-size:.82rem;font-weight:600;color:var(--np-error);margin-top:7px}
.np .np-err:empty{display:none}
.np .np-err::before{content:"!";flex:none;display:grid;place-items:center;width:16px;height:16px;margin-top:2px;border-radius:50%;background:var(--np-error);color:#fff;font-size:.7rem;font-weight:800}
.np .form-text{font-size:.78rem;color:var(--np-muted);margin-top:8px}
.np .np-done{outline:none}
.np .np-done[hidden],.np form[hidden]{display:none}
.np .np-done:focus-visible{outline:2px solid var(--np-accent);outline-offset:4px;border-radius:10px}
.np-done-ico{display:grid;place-items:center;width:44px;height:44px;border-radius:50%;background:#dcfce7;color:var(--np-ok);margin-bottom:10px}
.np-done-ico svg{width:22px;height:22px}
.np-done b[data-email]{overflow-wrap:anywhere}

/* 1. Centered popup */
.np-modal{position:fixed;inset:0;width:min(680px,calc(100% - 32px));max-height:calc(100dvh - 32px);margin:auto;border-radius:20px;overflow:auto;overscroll-behavior:contain}
.np-modal[open]{animation:np-pop .22s cubic-bezier(.2,.8,.2,1)}
.np-modal::backdrop{background:rgba(17,20,28,.55)}
.np-split{display:grid;grid-template-columns:240px 1fr}
.np-art{position:relative;min-height:100%;padding:22px;color:#fff;display:flex;flex-direction:column;justify-content:flex-end;
  background:radial-gradient(circle at 70% 26%,#ffe4b5 0 22px,rgba(255,228,181,.25) 23px 36px,transparent 37px),
    linear-gradient(160deg,transparent 52%,#3f1d2e 52%),linear-gradient(200deg,transparent 60%,#5b2140 60%),linear-gradient(180deg,#f59e7a,#be4a6a 60%,#7a1f45)}
.np-art b{font-size:1.05rem;line-height:1.25}
.np-art span{font-size:.8rem;opacity:.9}
.np-main{position:relative;padding:34px 30px 26px}
.np-field{margin-top:16px}
.np-foot{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:6px 12px;margin-top:14px}
.np .np-foot .form-text{margin:0}

/* 2. Corner slide-in */
.np-corner{position:fixed;inset:auto 20px 20px auto;width:min(360px,calc(100% - 24px));margin:0;border-radius:16px}
.np-corner[open]{animation:np-slide .3s cubic-bezier(.2,.8,.2,1)}
.np-corner-in{position:relative;padding:20px 20px 18px}
.np-corner-head{display:flex;gap:12px;align-items:flex-start;padding-right:30px}
.np-badge{flex:none;display:grid;place-items:center;width:40px;height:40px;border-radius:12px;background:#ffe4e6;color:var(--np-accent-dark)}
.np-badge svg{width:22px;height:22px}
.np-corner .np-title{font-size:1.05rem;margin:0 0 2px}
.np-corner .np-text{font-size:.86rem}
.np-corner form{margin-top:14px}
.np-corner .np-done{margin-top:12px}
.np-corner .btn-close{top:8px;right:8px}

/* 3. Bottom bar */
.np-bar{position:fixed;inset:auto 0 0 0;width:100%;max-width:none;margin:0;border-radius:16px 16px 0 0;border-top:3px solid var(--np-accent)}
.np-bar[open]{animation:np-rise .3s cubic-bezier(.2,.8,.2,1)}
.np-bar-in{position:relative;display:flex;flex-wrap:wrap;align-items:center;gap:12px 28px;max-width:1040px;margin:0 auto;padding:18px 60px 18px 24px}
.np-bar-copy{flex:1 1 280px;min-width:0}
.np-bar .np-title{font-size:1.05rem;margin:0}
.np-bar .np-text{font-size:.86rem}
.np-bar form,.np-bar .np-done{flex:1 1 340px;min-width:0}
.np-bar .btn-close{top:50%;right:12px;margin-top:-18px}
.np-bar .np-done:not([hidden]){display:flex;gap:12px;align-items:center}
.np-bar .np-done .np-done-ico{margin:0;flex:none}

@keyframes np-pop{from{opacity:0;transform:translateY(10px) scale(.97)}}
@keyframes np-slide{from{opacity:0;transform:translateX(24px)}}
@keyframes np-rise{from{transform:translateY(100%)}}
@media (max-width:600px){.np-split{grid-template-columns:1fr}.np-art{min-height:120px;padding:18px}.np-main{padding:26px 20px 22px}
  .np-corner{inset:auto 12px 12px 12px;width:auto}.np-bar-in{padding:16px 56px 16px 18px}.np-bar .btn-close{top:12px;margin-top:0}}
@media (max-width:420px){.np .np-row{flex-direction:column}}
@media (prefers-reduced-motion:reduce){.np-modal[open],.np-corner[open],.np-bar[open]{animation:none}.np .form-control,.np .np-btn{transition:none}}
JavaScript
// Opening and closing: buttons with data-open="id" open a dialog; showModal() when it has data-modal, show() otherwise.
// Esc closes both kinds, a backdrop click closes the modal one, and focus returns to the button that opened it.
// The forms are a front-end demo: nothing is sent. Errors are written into the element each input points to with
// aria-describedby; a valid submit swaps in the success state. Replace that branch with your fetch() call.
(function () {
  document.querySelectorAll('[data-open]').forEach(function (btn) {
    btn.addEventListener('click', function () {
      var dialog = document.getElementById(btn.getAttribute('data-open'));
      if (!dialog || dialog.open) return;
      dialog.opener = btn;
      if (dialog.hasAttribute('data-modal')) dialog.showModal(); else dialog.show();
      if (!dialog.hasAttribute('data-modal')) { var first = dialog.querySelector('input,button:not([data-close])'); if (first) first.focus(); }
    });
  });
  document.querySelectorAll('dialog.np').forEach(function (dialog) {
    dialog.querySelectorAll('[data-close]').forEach(function (b) { b.addEventListener('click', function () { dialog.close(); }); });
    dialog.addEventListener('close', function () { if (dialog.opener) dialog.opener.focus(); });
    // Esc for non-modal dialogs (modal ones get it from the browser)
    dialog.addEventListener('keydown', function (e) { if (e.key === 'Escape' && !dialog.matches(':modal')) { e.preventDefault(); dialog.close(); } });
    // light dismiss: a click that lands on the dialog element itself is a click on the backdrop
    dialog.addEventListener('click', function (e) { if (e.target === dialog && dialog.matches(':modal')) dialog.close(); });
  });

  function check(el) {
    var msg = el.checkValidity() ? '' : el.getAttribute(el.validity.valueMissing ? 'data-missing' : 'data-bad');
    var err = document.getElementById(el.getAttribute('data-err'));
    if (err) err.textContent = msg || '';
    el.classList.toggle('is-invalid', !!msg);
    if (msg) el.setAttribute('aria-invalid', 'true'); else el.removeAttribute('aria-invalid');
    return !msg;
  }
  document.querySelectorAll('form[data-nf]').forEach(function (form) {
    var done = document.getElementById(form.getAttribute('data-nf'));
    var email = form.querySelector('input[type=email]');
    form.noValidate = true;
    form.addEventListener('submit', function (e) {
      e.preventDefault();
      if (!check(email)) { email.focus(); return; }
      done.querySelectorAll('[data-email]').forEach(function (n) { n.textContent = email.value; });
      form.hidden = true; done.hidden = false; done.focus();
    });
    email.addEventListener('input', function () { if (email.classList.contains('is-invalid')) check(email); });
  });
})();

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <symbol id="np-i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6.5 6.5l11 11M17.5 6.5l-11 11"/></symbol>
  <symbol id="np-i-mail" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3.5 6.5L12 13l8.5-6.5"/></symbol>
  <symbol id="np-i-tick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.5l4.5 4.5L19 7.5"/></symbol>
  <symbol id="np-i-vase" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M9 3h6M10 3v3c-3 1.5-5 4.5-5 8a7 7 0 0 0 14 0c0-3.5-2-6.5-5-8V3M6 14h12"/></symbol>
</svg>

<div>
  <span class="mb-3 block text-[.72rem] tracking-[.12em] text-[#5a5f6c] uppercase">Centered popup</span>
  <div class="mb-4 flex flex-wrap items-center gap-x-4 gap-y-2.5"><button class="inline-flex cursor-pointer items-center gap-2 rounded-[10px] bg-np-accent px-4 py-2.5 text-[.9rem] font-[650] text-white transition-colors duration-150 hover:bg-np-accent-dark focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-np-accent motion-reduce:transition-none" type="button" data-open="np-dispatch" aria-haspopup="dialog"><svg class="size-[18px]" aria-hidden="true"><use href="#np-i-mail"/></svg>Open the popup</button><p class="text-[.86rem] text-[#5a5f6c]">Modal: Esc, the close button, Not now or a backdrop click closes it.</p></div>
  <div class="stage stage--center" data-preview="np-dispatch" aria-hidden="true" inert></div>
</div>

<div>
  <span class="mb-3 block text-[.72rem] tracking-[.12em] text-[#5a5f6c] uppercase">Corner slide-in</span>
  <div class="mb-4 flex flex-wrap items-center gap-x-4 gap-y-2.5"><button class="inline-flex cursor-pointer items-center gap-2 rounded-[10px] bg-np-accent px-4 py-2.5 text-[.9rem] font-[650] text-white transition-colors duration-150 hover:bg-np-accent-dark focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-np-accent motion-reduce:transition-none" type="button" data-open="np-kiln" aria-haspopup="dialog"><svg class="size-[18px]" aria-hidden="true"><use href="#np-i-mail"/></svg>Show the slide-in</button><p class="text-[.86rem] text-[#5a5f6c]">Non-modal: the page stays usable. Esc or the close button dismisses it.</p></div>
  <div class="stage stage--corner" data-preview="np-kiln" aria-hidden="true" inert></div>
</div>

<div>
  <span class="mb-3 block text-[.72rem] tracking-[.12em] text-[#5a5f6c] uppercase">Bottom bar</span>
  <div class="mb-4 flex flex-wrap items-center gap-x-4 gap-y-2.5"><button class="inline-flex cursor-pointer items-center gap-2 rounded-[10px] bg-np-accent px-4 py-2.5 text-[.9rem] font-[650] text-white transition-colors duration-150 hover:bg-np-accent-dark focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-np-accent motion-reduce:transition-none" type="button" data-open="np-halden" aria-haspopup="dialog"><svg class="size-[18px]" aria-hidden="true"><use href="#np-i-mail"/></svg>Show the bar</button><p class="text-[.86rem] text-[#5a5f6c]">Non-modal, full width, for a quieter ask.</p></div>
  <div class="stage stage--bar" data-preview="np-halden" aria-hidden="true" inert></div>
</div>

<!-- 1. Centered popup (showModal) -->
<dialog class="np fixed inset-0 m-auto max-h-[calc(100dvh-32px)] w-[min(680px,calc(100%-32px))] overflow-auto overscroll-contain rounded-[20px] bg-white p-0 text-[.92rem] leading-normal text-np-ink shadow-[0_0_0_1px_rgba(26,29,36,.06),0_30px_70px_-24px_rgba(26,29,36,.55)] backdrop:bg-[rgba(17,20,28,.55)] open:animate-np-pop motion-reduce:open:animate-none" id="np-dispatch" data-modal closedby="any" aria-labelledby="np-dispatch-title">
  <div class="grid grid-cols-[240px_1fr] max-[600px]:grid-cols-1">
    <div class="relative flex min-h-full flex-col justify-end bg-[radial-gradient(circle_at_70%_26%,#ffe4b5_0_22px,rgba(255,228,181,.25)_23px_36px,transparent_37px),linear-gradient(160deg,transparent_52%,#3f1d2e_52%),linear-gradient(200deg,transparent_60%,#5b2140_60%),linear-gradient(180deg,#f59e7a,#be4a6a_60%,#7a1f45)] p-[22px] text-white max-[600px]:min-h-[120px] max-[600px]:p-[18px]" aria-hidden="true"><b class="text-[1.05rem] leading-[1.25]">Northline</b><span class="text-[.8rem] opacity-90">Read by 52,000 walkers</span></div>
    <div class="relative px-[30px] pt-[34px] pb-[26px] max-[600px]:px-5 max-[600px]:pt-[26px] max-[600px]:pb-[22px]">
      <button class="absolute top-2.5 right-2.5 z-2 grid size-9 cursor-pointer place-items-center rounded-[10px] bg-white/90 text-np-muted hover:bg-[#eceef2] hover:text-np-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-np-accent" type="button" data-close aria-label="Close"><svg class="size-5" aria-hidden="true"><use href="#np-i-x"/></svg></button>
      <form action="#" method="post" data-nf="np-dispatch-done">
        <p class="text-[.72rem] font-bold tracking-[.1em] text-np-accent-dark uppercase">Free, every Sunday</p>
        <h2 class="mt-1 mb-1.5 text-[1.4rem] leading-[1.2] font-[750] tracking-[-.02em]" id="np-dispatch-title">Get the Sunday Dispatch</h2>
        <p class="text-np-muted">Three routes worth the drive, one gear test we paid for ourselves, and a long read for the evening.</p>
        <div class="mt-4">
          <label class="mb-1.5 block text-[.84rem] font-[650]" for="np-dispatch-email">Email address</label>
          <div class="flex gap-2 max-[420px]:flex-col">
            <input class="block w-full min-w-0 flex-1 basis-0 rounded-[10px] border-[1.5px] border-np-line bg-white px-[13px] py-2.5 text-np-ink transition-[border-color,box-shadow] duration-150 focus:border-np-accent focus:shadow-[0_0_0_4px_rgba(190,18,60,.22)] focus:outline-none [&.is-invalid]:border-np-error motion-reduce:transition-none max-[420px]:basis-auto" id="np-dispatch-email" name="email" type="email" autocomplete="email" required autofocus aria-describedby="np-dispatch-err np-dispatch-hint" data-err="np-dispatch-err"
              data-missing="Enter your email address to subscribe." data-bad="Enter an email address with an @ and a domain, like [email protected].">
            <button class="flex-none cursor-pointer rounded-[10px] bg-np-accent px-[18px] py-2.5 font-[650] text-white transition-colors duration-150 hover:bg-np-accent-dark focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-np-accent motion-reduce:transition-none" type="submit">Subscribe</button>
          </div>
          <p class="mt-[7px] flex items-start gap-[7px] text-[.82rem] font-semibold text-np-error empty:hidden before:mt-0.5 before:grid before:size-4 before:flex-none before:place-items-center before:rounded-full before:bg-np-error before:text-[.7rem] before:font-extrabold before:text-white before:content-['!']" id="np-dispatch-err"></p>
        </div>
        <div class="mt-3.5 flex flex-wrap items-center justify-between gap-x-3 gap-y-1.5">
          <p class="text-[.78rem] text-np-muted" id="np-dispatch-hint">One email a week. <a class="font-semibold text-np-accent-dark underline underline-offset-2 focus-visible:rounded-[3px] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-np-accent" href="#privacy">Privacy notice</a></p>
          <button class="-ml-1.5 cursor-pointer rounded-md px-1.5 py-1 text-[.86rem] font-semibold text-np-muted underline underline-offset-2 hover:text-np-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-np-accent" type="button" data-close>Not now</button>
        </div>
      </form>
      <div class="focus-visible:rounded-[10px] focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-np-accent" id="np-dispatch-done" tabindex="-1" hidden>
        <span class="mb-2.5 grid size-11 place-items-center rounded-full bg-[#dcfce7] text-np-ok" aria-hidden="true"><svg class="size-[22px]"><use href="#np-i-tick"/></svg></span>
        <h2 class="mt-1 mb-1.5 text-[1.4rem] leading-[1.2] font-[750] tracking-[-.02em]">Check your inbox</h2>
        <p class="text-np-muted">We sent a confirmation link to <b class="wrap-anywhere" data-email>you</b>. The next Dispatch goes out on Sunday at 7am.</p>
        <div class="mt-3.5"><button class="cursor-pointer rounded-[10px] bg-np-accent px-[18px] py-2.5 font-[650] text-white transition-colors duration-150 hover:bg-np-accent-dark focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-np-accent motion-reduce:transition-none" type="button" data-close>Back to the article</button></div>
      </div>
    </div>
  </div>
</dialog>

<!-- 2. Corner slide-in (show) -->
<dialog class="np fixed inset-auto right-5 bottom-5 m-0 w-[min(360px,calc(100%-24px))] rounded-2xl bg-white p-0 text-[.92rem] leading-normal text-np-ink shadow-[0_0_0_1px_rgba(26,29,36,.06),0_30px_70px_-24px_rgba(26,29,36,.55)] open:animate-np-slide motion-reduce:open:animate-none max-[600px]:right-3 max-[600px]:bottom-3 max-[600px]:left-3 max-[600px]:w-auto" id="np-kiln" aria-labelledby="np-kiln-title">
  <div class="relative px-5 pt-5 pb-[18px]">
    <button class="absolute top-2 right-2 z-2 grid size-9 cursor-pointer place-items-center rounded-[10px] bg-white/90 text-np-muted hover:bg-[#eceef2] hover:text-np-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-np-accent" type="button" data-close aria-label="Close"><svg class="size-5" aria-hidden="true"><use href="#np-i-x"/></svg></button>
    <div class="flex items-start gap-3 pr-[30px]">
      <span class="grid size-10 flex-none place-items-center rounded-xl bg-[#ffe4e6] text-np-accent-dark" aria-hidden="true"><svg class="size-[22px]"><use href="#np-i-vase"/></svg></span>
      <div><h2 class="mb-0.5 text-[1.05rem] leading-[1.2] font-[750] tracking-[-.02em]" id="np-kiln-title">Enjoying part 2?</h2>
        <p class="text-[.86rem] text-np-muted">Get the other four parts of our glazing series by email as each one is published.</p></div>
    </div>
    <form class="mt-3.5" action="#" method="post" data-nf="np-kiln-done">
      <label class="mb-1.5 block text-[.84rem] font-[650]" for="np-kiln-email">Email address</label>
      <div class="flex flex-col gap-2">
        <input class="block w-full min-w-0 rounded-[10px] border-[1.5px] border-np-line bg-white px-[13px] py-2.5 text-np-ink transition-[border-color,box-shadow] duration-150 focus:border-np-accent focus:shadow-[0_0_0_4px_rgba(190,18,60,.22)] focus:outline-none [&.is-invalid]:border-np-error motion-reduce:transition-none" id="np-kiln-email" name="email" type="email" autocomplete="email" required aria-describedby="np-kiln-err np-kiln-hint" data-err="np-kiln-err"
          data-missing="Enter your email address." data-bad="Enter an email address with an @ and a domain, like [email protected].">
        <button class="cursor-pointer rounded-[10px] bg-np-accent px-[18px] py-2.5 font-[650] text-white transition-colors duration-150 hover:bg-np-accent-dark focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-np-accent motion-reduce:transition-none" type="submit">Send me the series</button>
      </div>
      <p class="mt-[7px] flex items-start gap-[7px] text-[.82rem] font-semibold text-np-error empty:hidden before:mt-0.5 before:grid before:size-4 before:flex-none before:place-items-center before:rounded-full before:bg-np-error before:text-[.7rem] before:font-extrabold before:text-white before:content-['!']" id="np-kiln-err"></p>
      <p class="mt-2 text-[.78rem] text-np-muted" id="np-kiln-hint">Five emails from Kiln & Clay, then nothing unless you ask.</p>
    </form>
    <div class="mt-3 focus-visible:rounded-[10px] focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-np-accent" id="np-kiln-done" tabindex="-1" hidden>
      <p class="text-np-muted"><b>Done.</b> Part 3 will reach <b class="wrap-anywhere" data-email>you</b> on 2 October, once you confirm the email we just sent.</p>
    </div>
  </div>
</dialog>

<!-- 3. Bottom bar (show) -->
<dialog class="np fixed inset-x-0 top-auto bottom-0 m-0 w-full max-w-none rounded-t-2xl border-t-3 border-np-accent bg-white p-0 text-[.92rem] leading-normal text-np-ink shadow-[0_0_0_1px_rgba(26,29,36,.06),0_30px_70px_-24px_rgba(26,29,36,.55)] open:animate-np-rise motion-reduce:open:animate-none" id="np-halden" aria-labelledby="np-halden-title">
  <div class="relative mx-auto flex max-w-[1040px] flex-wrap items-center gap-x-7 gap-y-3 py-[18px] pr-[60px] pl-6 max-[600px]:py-4 max-[600px]:pr-14 max-[600px]:pl-[18px]">
    <button class="absolute top-1/2 right-3 z-2 -mt-[18px] max-[600px]:top-3 max-[600px]:mt-0 grid size-9 cursor-pointer place-items-center rounded-[10px] bg-white/90 text-np-muted hover:bg-[#eceef2] hover:text-np-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-np-accent" type="button" data-close aria-label="Close"><svg class="size-5" aria-hidden="true"><use href="#np-i-x"/></svg></button>
    <div class="min-w-0 flex-[1_1_280px]"><h2 class="text-[1.05rem] leading-[1.2] font-[750] tracking-[-.02em]" id="np-halden-title">The Thursday deals digest</h2>
      <p class="text-[.86rem] text-np-muted">Real discounts on tents, boots and stoves from Halden Outdoor. Weekly, never daily.</p></div>
    <form class="min-w-0 flex-[1_1_340px]" action="#" method="post" data-nf="np-halden-done">
      <label class="sr-only" for="np-halden-email">Email address</label>
      <div class="flex gap-2 max-[420px]:flex-col">
        <input class="block w-full min-w-0 flex-1 basis-0 rounded-[10px] border-[1.5px] border-np-line bg-white px-[13px] py-2.5 text-np-ink placeholder:text-[#626774] transition-[border-color,box-shadow] duration-150 focus:border-np-accent focus:shadow-[0_0_0_4px_rgba(190,18,60,.22)] focus:outline-none [&.is-invalid]:border-np-error motion-reduce:transition-none max-[420px]:basis-auto" id="np-halden-email" name="email" type="email" autocomplete="email" required placeholder="Email address" aria-describedby="np-halden-err" data-err="np-halden-err"
          data-missing="Enter your email address." data-bad="Enter an email address with an @ and a domain, like [email protected].">
        <button class="flex-none cursor-pointer rounded-[10px] bg-np-accent px-[18px] py-2.5 font-[650] text-white transition-colors duration-150 hover:bg-np-accent-dark focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-np-accent motion-reduce:transition-none" type="submit">Get the digest</button>
      </div>
      <p class="mt-[7px] flex items-start gap-[7px] text-[.82rem] font-semibold text-np-error empty:hidden before:mt-0.5 before:grid before:size-4 before:flex-none before:place-items-center before:rounded-full before:bg-np-error before:text-[.7rem] before:font-extrabold before:text-white before:content-['!']" id="np-halden-err"></p>
    </form>
    <div class="flex min-w-0 flex-[1_1_340px] items-center gap-3 focus-visible:rounded-[10px] focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-np-accent" id="np-halden-done" tabindex="-1" hidden>
      <span class="grid size-11 flex-none place-items-center rounded-full bg-[#dcfce7] text-np-ok" aria-hidden="true"><svg class="size-[22px]"><use href="#np-i-tick"/></svg></span>
      <p class="text-[.86rem] text-np-muted">Confirm the email we sent to <b class="wrap-anywhere" data-email>you</b> and Thursday's digest is yours.</p>
    </div>
  </div>
</dialog>
CSS
@theme {
  --color-np-ink: #1a1d24;
  --color-np-muted: #535866;
  --color-np-line: #7b808d;
  --color-np-accent: #be123c;
  --color-np-accent-dark: #9f1239;
  --color-np-error: #b42318;
  --color-np-ok: #15803d;
  --animate-np-pop: np-pop .22s cubic-bezier(.2,.8,.2,1);
  --animate-np-slide: np-slide .3s cubic-bezier(.2,.8,.2,1);
  --animate-np-rise: np-rise .3s cubic-bezier(.2,.8,.2,1);
  @keyframes np-pop { from { opacity: 0; transform: translateY(10px) scale(.97); } }
  @keyframes np-slide { from { opacity: 0; transform: translateX(24px); } }
  @keyframes np-rise { from { transform: translateY(100%); } }
}
/* scroll lock while the modal popup is open */
html:has(dialog[data-modal][open]) { overflow: hidden; }
JavaScript
// Opening and closing: buttons with data-open="id" open a dialog; showModal() when it has data-modal, show() otherwise.
// Esc closes both kinds, a backdrop click closes the modal one, and focus returns to the button that opened it.
// The forms are a front-end demo: nothing is sent. Errors are written into the element each input points to with
// aria-describedby; a valid submit swaps in the success state. Replace that branch with your fetch() call.
(function () {
  document.querySelectorAll('[data-open]').forEach(function (btn) {
    btn.addEventListener('click', function () {
      var dialog = document.getElementById(btn.getAttribute('data-open'));
      if (!dialog || dialog.open) return;
      dialog.opener = btn;
      if (dialog.hasAttribute('data-modal')) dialog.showModal(); else dialog.show();
      if (!dialog.hasAttribute('data-modal')) { var first = dialog.querySelector('input,button:not([data-close])'); if (first) first.focus(); }
    });
  });
  document.querySelectorAll('dialog.np').forEach(function (dialog) {
    dialog.querySelectorAll('[data-close]').forEach(function (b) { b.addEventListener('click', function () { dialog.close(); }); });
    dialog.addEventListener('close', function () { if (dialog.opener) dialog.opener.focus(); });
    // Esc for non-modal dialogs (modal ones get it from the browser)
    dialog.addEventListener('keydown', function (e) { if (e.key === 'Escape' && !dialog.matches(':modal')) { e.preventDefault(); dialog.close(); } });
    // light dismiss: a click that lands on the dialog element itself is a click on the backdrop
    dialog.addEventListener('click', function (e) { if (e.target === dialog && dialog.matches(':modal')) dialog.close(); });
  });

  function check(el) {
    var msg = el.checkValidity() ? '' : el.getAttribute(el.validity.valueMissing ? 'data-missing' : 'data-bad');
    var err = document.getElementById(el.getAttribute('data-err'));
    if (err) err.textContent = msg || '';
    el.classList.toggle('is-invalid', !!msg);
    if (msg) el.setAttribute('aria-invalid', 'true'); else el.removeAttribute('aria-invalid');
    return !msg;
  }
  document.querySelectorAll('form[data-nf]').forEach(function (form) {
    var done = document.getElementById(form.getAttribute('data-nf'));
    var email = form.querySelector('input[type=email]');
    form.noValidate = true;
    form.addEventListener('submit', function (e) {
      e.preventDefault();
      if (!check(email)) { email.focus(); return; }
      done.querySelectorAll('[data-email]').forEach(function (n) { n.textContent = email.value; });
      form.hidden = true; done.hidden = false; done.focus();
    });
    email.addEventListener('input', function () { if (email.classList.contains('is-invalid')) check(email); });
  });
})();

About this set

Three newsletter prompts built on the native dialog element, designed to be dismissible and honest. Centered popup opens with showModal(): an illustrated panel beside a magazine sign-up, a visible close button, a plain Not now link and a privacy line. Focus moves to the email field, the page behind becomes inert and stops scrolling, and Esc or a click on the backdrop closes it. Corner slide-in opens with show() in the bottom right, so the page stays usable while a reader decides whether to get the rest of an article series. Bottom bar is a full-width, non-modal strip for a weekly deals digest. All three open from a button, never on page load, and the copy avoids guilt-trip wording. A short vanilla script opens each dialog, adds Esc to the non-modal ones, returns focus to the button that opened it, and handles the form: inline error on a bad address, a confirmation on a good one. Nothing is sent. The demo page also draws an inert preview of each dialog so you can see the design without opening it.

What’s included

  • Native dialog: showModal() for the popup, show() for the slide-in and bar
  • Close button, Not now, Esc and backdrop click all dismiss
  • Focus returns to the opening button on close
  • Enter animations switch off under reduced motion
  • Bootstrap version uses .btn-close, .form-control and .btn inside the dialog, no Bootstrap JavaScript

Accessibility

Each dialog is named by its heading with aria-labelledby and its close button has aria-label="Close". The modal moves focus to the email field and traps it until closed; the non-modal dialogs move focus to their field and close on Esc. Focus returns to the trigger, errors are tied to the field with aria-describedby and aria-invalid, and every control shows a visible focus ring. Animations and transitions switch off under prefers-reduced-motion.

Customise it

Set --np-accent, --np-accent-dark and --np-ring on .np for the brand colour. The popup illustration is the .np-art gradient stack, and positions live on .np-corner and .np-bar. In Tailwind change the np-* colours and the np-pop, np-slide and np-rise animations in @theme.