Open demo

CSS code

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <symbol id="mx-i-tick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M8 12.5l2.8 2.8L16.5 9.5"/></symbol>
  <symbol id="mx-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="mx-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="mx-i-jar" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3h8v3H8zM7 6h10a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2zM5 12h14"/></symbol>
</svg>

<div class="demo-grid">
<div><span class="lbl">:user-invalid validation</span>
<section class="mx" aria-labelledby="fc-title">
  <form class="mx-form" action="#" method="post" data-nf="fc-done">
    <h3 class="mx-title" id="fc-title">Ferment Club letters</h3>
    <p class="mx-text">A fortnightly email on kimchi, kefir, miso and the science of letting food get on with it.</p>
    <div class="mx-field">
      <label class="mx-label" for="fc-email">Email address</label>
      <input class="mx-input" id="fc-email" name="email" type="email" autocomplete="email" required aria-describedby="fc-email-msg">
      <p class="mx-msg" id="fc-email-msg">Enter an email address, like [email protected].</p>
    </div>
    <div class="mx-field">
      <label class="mx-check"><input id="fc-consent" name="consent" type="checkbox" required aria-describedby="fc-consent-msg"><span>I agree to receive Ferment Club emails. I can unsubscribe at any time.</span></label>
      <p class="mx-msg" id="fc-consent-msg"><span>Required, so we know you asked. <a href="#privacy">Privacy notice</a></span></p>
    </div>
    <button class="mx-btn" type="submit">Subscribe</button>
  </form>
  <div class="mx-done" id="fc-done" tabindex="-1" hidden><svg aria-hidden="true"><use href="#mx-i-tick"/></svg><div><b>Almost there</b><p>Confirm <span data-email>your address</span> from the email we just sent.</p></div></div>
</section>
</div>

<div><span class="lbl">:has() reveals extra fields</span>
<section class="mx" aria-labelledby="fw-title">
  <form class="mx-form" action="#" method="post" data-nf="fw-done">
    <h3 class="mx-title" id="fw-title">Ferment Club workshops</h3>
    <p class="mx-text">Pick a pace. Tick the box below and a second question appears, with no script.</p>
    <fieldset class="mx-set">
      <legend>How often?</legend>
      <div class="mx-seg">
        <label><input type="radio" name="freq" value="weekly" checked>Weekly</label>
        <label><input type="radio" name="freq" value="monthly">Monthly</label>
      </div>
    </fieldset>
    <div class="mx-field mx-day">
      <label class="mx-label" for="fw-day">Which morning? (optional)</label>
      <select class="mx-input" id="fw-day" name="day"><option>Monday</option><option selected>Thursday</option><option>Saturday</option></select>
    </div>
    <div class="mx-field">
      <label class="mx-label" for="fw-email">Email address</label>
      <input class="mx-input" id="fw-email" name="email" type="email" autocomplete="email" required aria-describedby="fw-email-msg">
      <p class="mx-msg" id="fw-email-msg">Enter an email address, like [email protected].</p>
    </div>
    <div class="mx-field">
      <label class="mx-check"><input class="mx-reveal" name="local" type="checkbox"><span>Also tell me about workshops near me (optional)</span></label>
      <div class="mx-more">
        <div class="mx-field">
          <label class="mx-label" for="fw-city">Nearest city (optional)</label>
          <select class="mx-input" id="fw-city" name="city"><option value="">Choose a city</option><option>Bristol</option><option>Leeds</option><option>Glasgow</option><option>Norwich</option></select>
        </div>
      </div>
    </div>
    <button class="mx-btn" type="submit">Subscribe</button>
  </form>
  <div class="mx-done" id="fw-done" tabindex="-1" hidden><svg aria-hidden="true"><use href="#mx-i-tick"/></svg><div><b>Saved</b><p>Check <span data-email>your inbox</span> to confirm. Workshop dates come with the next issue.</p></div></div>
</section>
</div>
</div>

<div>
  <span class="lbl">Popover slide-in</span>
  <div class="demo-row"><button class="mx-open" type="button" popovertarget="mx-pop"><svg aria-hidden="true"><use href="#mx-i-mail"/></svg>Open the slide-in</button><span>Esc or a click outside closes it. No JavaScript.</span></div>
  <div class="stage" data-preview="mx-pop" aria-hidden="true" inert></div>
</div>

<div>
  <span class="lbl">Container query card</span>
  <div class="demo-cq">
    <div><small>In a 280px sidebar</small>
      <div class="mx-cq-wrap"><section class="mx mx-cq" aria-labelledby="cq1-title">
        <div class="mx-cq-art" aria-hidden="true"><svg><use href="#mx-i-jar"/></svg></div>
        <div><h3 class="mx-title" id="cq1-title">The Brine Times</h3><p class="mx-text">Seasonal pickles, one recipe a month.</p>
          <form class="mx-form" action="#" method="post" data-nf="cq1-done">
            <label class="mx-label" for="cq1-email">Email address</label>
            <div class="mx-row"><input class="mx-input" id="cq1-email" name="email" type="email" autocomplete="email" required><button class="mx-btn" type="submit">Subscribe</button></div>
          </form>
          <div class="mx-done" id="cq1-done" tabindex="-1" hidden><svg aria-hidden="true"><use href="#mx-i-tick"/></svg><div><b>Thanks</b><p>Confirm <span data-email>your address</span> to start.</p></div></div>
        </div>
      </section></div>
    </div>
    <div><small>In the main column</small>
      <div class="mx-cq-wrap"><section class="mx mx-cq" aria-labelledby="cq2-title">
        <div class="mx-cq-art" aria-hidden="true"><svg><use href="#mx-i-jar"/></svg></div>
        <div><h3 class="mx-title" id="cq2-title">The Brine Times</h3><p class="mx-text">Seasonal pickles, one recipe a month.</p>
          <form class="mx-form" action="#" method="post" data-nf="cq2-done">
            <label class="mx-label" for="cq2-email">Email address</label>
            <div class="mx-row"><input class="mx-input" id="cq2-email" name="email" type="email" autocomplete="email" required><button class="mx-btn" type="submit">Subscribe</button></div>
          </form>
          <div class="mx-done" id="cq2-done" tabindex="-1" hidden><svg aria-hidden="true"><use href="#mx-i-tick"/></svg><div><b>Thanks</b><p>Confirm <span data-email>your address</span> to start.</p></div></div>
        </div>
      </section></div>
    </div>
  </div>
</div>

<!-- the popover itself: opened by popovertarget, closed by Esc, a click outside or the close button -->
<div class="mx mx-pop" id="mx-pop" popover role="dialog" aria-labelledby="mx-pop-title">
  <button class="mx-x" type="button" popovertarget="mx-pop" popovertargetaction="hide" aria-label="Close"><svg aria-hidden="true"><use href="#mx-i-x"/></svg></button>
  <h3 class="mx-title" id="mx-pop-title">Before you go back to the recipe</h3>
  <p class="mx-text">Get the next Ferment Club letter: a one-jar sauerkraut that's ready in a week.</p>
  <form class="mx-form" action="#" method="post" data-nf="mx-pop-done">
    <div class="mx-field">
      <label class="mx-label" for="mx-pop-email">Email address</label>
      <input class="mx-input" id="mx-pop-email" name="email" type="email" autocomplete="email" required aria-describedby="mx-pop-msg">
      <p class="mx-msg" id="mx-pop-msg">Enter an email address, like [email protected].</p>
    </div>
    <button class="mx-btn" type="submit">Send me the next letter</button>
  </form>
  <div class="mx-done" id="mx-pop-done" tabindex="-1" hidden><svg aria-hidden="true"><use href="#mx-i-tick"/></svg><div><b>On its way</b><p>Confirm <span data-email>your address</span> and the sauerkraut letter is yours.</p></div></div>
</div>
CSS
/* Newsletter forms that lean on modern CSS instead of JavaScript.
   - :user-invalid / :user-valid: errors and ticks appear only after someone has edited a field or tried to submit.
   - :has(): a field's message turns into an error, an optional field appears when a box is ticked, and the button
     looks ready once the form is valid (it is never disabled, so the browser's own checks still run on submit).
   - popover + @starting-style: a slide-in that opens, closes on Esc or a click outside, and animates both ways, with no script.
   - Container queries: one card that lays itself out by the width of its container, not the viewport.
   The only script stops the demo submit and shows the success message. */
.mx{--mx-ink:#10231c;--mx-muted:#465a52;--mx-line:#738880;--mx-soft:#d6e2dc;--mx-accent:#047857;--mx-accent-dark:#065f46;--mx-ring:rgba(4,120,87,.22);--mx-tint:#e6f4ee;
  --mx-error:#b42318;--mx-idle:#d3ddd8;--mx-idle-ink:#2c4038;
  background:#fff;color:var(--mx-ink);border-radius:20px;padding:28px;font-size:.92rem;line-height:1.5;box-shadow:0 1px 2px rgba(16,35,28,.06),0 20px 44px -26px rgba(16,35,28,.4)}
.mx a{color:var(--mx-accent-dark);font-weight:600;text-underline-offset:2px}
.mx a:focus-visible{outline:2px solid var(--mx-accent);outline-offset:2px;border-radius:3px}
.mx-title{font-size:1.3rem;font-weight:750;letter-spacing:-.02em;line-height:1.25}
.mx-text{color:var(--mx-muted);margin:4px 0 18px}
.mx-field{margin-bottom:14px;min-width:0}
.mx-label{display:block;font-size:.84rem;font-weight:650;margin-bottom:6px}
.mx-input{display:block;width:100%;min-width:0;font:inherit;color:var(--mx-ink);background:#fff no-repeat right 12px center/18px;border:1.5px solid var(--mx-line);border-radius:10px;
  padding:10px 40px 10px 13px;transition:border-color .15s,box-shadow .15s}
.mx-input:focus{outline:none;border-color:var(--mx-accent);box-shadow:0 0 0 4px var(--mx-ring)}
input.mx-input:user-valid{border-color:var(--mx-accent);
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M5 12.5l4.5 4.5L19 7.5' fill='none' stroke='%23047857' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E")}
.mx-input:user-invalid{border-color:var(--mx-error)}
.mx-input:user-invalid:focus{box-shadow:0 0 0 4px rgba(180,35,24,.18)}
select.mx-input{appearance:none;cursor:pointer;padding-right:36px;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23465a52' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m7 10 5 5 5-5'/%3E%3C/svg%3E");background-size:16px}
.mx-msg{display:flex;gap:6px;align-items:flex-start;font-size:.8rem;color:var(--mx-muted);margin-top:6px}
.mx-field:has(:user-invalid) .mx-msg{color:var(--mx-error);font-weight:600}
.mx-field:has(:user-invalid) .mx-msg::before{content:"!";flex:none;display:grid;place-items:center;width:16px;height:16px;margin-top:1px;border-radius:50%;background:var(--mx-error);color:#fff;font-size:.7rem;font-weight:800}
.mx-check{display:flex;gap:10px;align-items:flex-start;font-size:.86rem;cursor:pointer}
.mx-check input{appearance:none;flex:none;width:18px;height:18px;margin:2px 0 0;border:1.5px solid var(--mx-line);border-radius:5px;background:#fff center/12px no-repeat;cursor:pointer;transition:background-color .15s,border-color .15s}
.mx-check input:checked{background-color:var(--mx-accent);border-color:var(--mx-accent);
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8.5l3 3 6-7' fill='none' stroke='%23fff' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E")}
.mx-check input:user-invalid{border-color:var(--mx-error)}
.mx-check input:focus-visible{outline:2px solid var(--mx-accent);outline-offset:2px}
.mx-check+.mx-msg{margin-left:28px}
/* the button is neutral until the form is valid, but never disabled */
.mx-btn{display:block;width:100%;font:inherit;font-weight:650;color:var(--mx-idle-ink);background:var(--mx-idle);border:0;border-radius:10px;padding:11px 16px;cursor:pointer;
  transition:background-color .2s,color .2s}
.mx-form:not(:has(:invalid)) .mx-btn{color:#fff;background:var(--mx-accent)}
.mx-form:not(:has(:invalid)) .mx-btn:hover{background:var(--mx-accent-dark)}
.mx-btn:focus-visible{outline:2px solid var(--mx-accent);outline-offset:3px}
.mx-done{outline:none;display:flex;gap:12px;align-items:flex-start;padding:14px 16px;border-radius:12px;background:var(--mx-tint)}
.mx-done[hidden],.mx-form[hidden]{display:none}
.mx-done:focus-visible{outline:2px solid var(--mx-accent);outline-offset:3px}
.mx-done svg{flex:none;width:24px;height:24px;color:var(--mx-accent)}
.mx-done b{display:block}
.mx-done p{color:var(--mx-muted);font-size:.86rem}
.mx-done span[data-email]{color:var(--mx-ink);font-weight:600;overflow-wrap:anywhere}

/* 2. :has() reveal */
.mx-seg{display:grid;grid-template-columns:1fr 1fr;gap:4px;padding:4px;border:1px solid var(--mx-soft);border-radius:12px;background:#f2f7f4}
.mx-seg label{position:relative;text-align:center;padding:8px;border-radius:9px;font-size:.86rem;font-weight:600;color:var(--mx-muted);cursor:pointer;transition:background-color .15s,color .15s,box-shadow .15s}
.mx-seg input{position:absolute;opacity:0;width:1px;height:1px;margin:0;pointer-events:none}
.mx-seg label:has(:checked){background:#fff;color:var(--mx-ink);box-shadow:0 1px 3px rgba(16,35,28,.18)}
.mx-seg label:has(:focus-visible){outline:2px solid var(--mx-accent);outline-offset:1px}
.mx-set{border:0;min-width:0;margin-bottom:14px}
.mx-set legend{font-size:.84rem;font-weight:650;padding:0;margin-bottom:6px}
.mx-more{display:none;margin:12px 0 0 28px;padding:12px 14px;border-left:3px solid var(--mx-accent);border-radius:0 10px 10px 0;background:#f4f9f6}
.mx-form:has(.mx-reveal:checked) .mx-more{display:block}
.mx-more .mx-field{margin:0}
.mx-day{display:none}
.mx-form:has([value=weekly]:checked) .mx-day{display:block}

/* 3. Popover slide-in */
.mx-open{display:inline-flex;align-items:center;gap:8px;font:inherit;font-size:.9rem;font-weight:650;color:#fff;background:#047857;border:0;border-radius:10px;padding:10px 16px;cursor:pointer;transition:background-color .15s}
.mx-open:hover{background:#065f46}
.mx-open:focus-visible{outline:2px solid #047857;outline-offset:3px}
.mx-open svg{width:18px;height:18px}
.mx-pop{position:fixed;inset:auto 20px 20px auto;width:min(360px,calc(100% - 24px));margin:0;border:0;padding:22px 22px 20px;
  opacity:0;translate:0 24px;transition:opacity .25s,translate .25s,display .25s allow-discrete,overlay .25s allow-discrete}
.mx-pop:popover-open{opacity:1;translate:0 0}
@starting-style{.mx-pop:popover-open{opacity:0;translate:0 24px}}
.mx-pop .mx-title{font-size:1.1rem;padding-right:34px}
.mx-pop .mx-text{margin-bottom:14px}
.mx-x{position:absolute;top:10px;right:10px;display:grid;place-items:center;width:34px;height:34px;padding:0;border:0;border-radius:9px;background:transparent;color:var(--mx-muted);cursor:pointer}
.mx-x:hover{background:#eef3f0;color:var(--mx-ink)}
.mx-x:focus-visible{outline:2px solid var(--mx-accent);outline-offset:1px}
.mx-x svg{width:20px;height:20px}

/* 4. Container query card: stacked when narrow, side by side from 520px of container width */
.mx-cq-wrap{container-type:inline-size}
.mx-cq{display:grid;gap:16px;padding:22px}
.mx-cq-art{display:grid;place-items:center;aspect-ratio:16/9;border-radius:14px;color:#fff;
  background:radial-gradient(circle at 18% 24%,#fbbf24 0 13%,transparent 14%),radial-gradient(circle at 84% 78%,#f97316 0 11%,transparent 12%),linear-gradient(135deg,#065f46,#047857 55%,#10b981)}
.mx-cq-art svg{width:40px;height:40px;opacity:.9}
.mx-cq .mx-text{margin-bottom:12px}
.mx-cq .mx-row{display:grid;gap:8px}
.mx-cq .mx-btn{color:#fff;background:var(--mx-accent)}
.mx-cq .mx-btn:hover{background:var(--mx-accent-dark)}
@container (min-width:520px){
  .mx-cq{grid-template-columns:170px 1fr;align-items:center;gap:22px}
  .mx-cq-art{aspect-ratio:1}
  .mx-cq .mx-row{grid-template-columns:1fr auto}
  .mx-cq .mx-btn{width:auto}
}

@media (max-width:420px){.mx{padding:22px 18px}.mx-cq{padding:18px}.mx-pop{inset:auto 12px 12px 12px;width:auto}}
@media (prefers-reduced-motion:reduce){.mx-input,.mx-check input,.mx-btn,.mx-seg label,.mx-open,.mx-pop{transition:none}}
JavaScript
// The only script: stop the demo submit and show the success message. The browser does all the validation, so the
// submit event only fires once every field is valid. Replace the body with your fetch() call.
document.querySelectorAll('form[data-nf]').forEach(function (form) {
  form.addEventListener('submit', function (e) {
    e.preventDefault();
    var done = document.getElementById(form.getAttribute('data-nf'));
    done.querySelector('[data-email]').textContent = form.elements.email.value;
    form.hidden = true; done.hidden = false; done.focus();
  });
});

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="mx-i-tick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M8 12.5l2.8 2.8L16.5 9.5"/></symbol>
  <symbol id="mx-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="mx-i-jar" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3h8v3H8zM7 6h10a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2zM5 12h14"/></symbol>
</svg>

<div class="demo-grid">
<div><span class="lbl">:user-invalid validation</span>
<section class="card mx" aria-labelledby="fc-title"><div class="card-body">
  <form action="#" method="post" data-nf="fc-done">
    <h3 class="mx-title" id="fc-title">Ferment Club letters</h3>
    <p class="mx-text">A fortnightly email on kimchi, kefir, miso and the science of letting food get on with it.</p>
    <div class="mx-field">
      <label class="form-label" for="fc-email">Email address</label>
      <input class="form-control" id="fc-email" name="email" type="email" autocomplete="email" required aria-describedby="fc-email-msg">
      <p class="form-text" id="fc-email-msg">Enter an email address, like [email protected].</p>
    </div>
    <div class="mx-field">
      <div class="form-check"><input class="form-check-input" id="fc-consent" name="consent" type="checkbox" required aria-describedby="fc-consent-msg"><label class="form-check-label" for="fc-consent">I agree to receive Ferment Club emails. I can unsubscribe at any time.</label></div>
      <p class="form-text" id="fc-consent-msg"><span>Required, so we know you asked. <a href="#privacy">Privacy notice</a></span></p>
    </div>
    <button class="btn mx-btn" type="submit">Subscribe</button>
  </form>
  <div class="mx-done" id="fc-done" tabindex="-1" hidden><svg aria-hidden="true"><use href="#mx-i-tick"/></svg><div><b>Almost there</b><p>Confirm <span data-email>your address</span> from the email we just sent.</p></div></div>
</div></section>
</div>

<div><span class="lbl">:has() reveals extra fields</span>
<section class="card mx" aria-labelledby="fw-title"><div class="card-body">
  <form action="#" method="post" data-nf="fw-done">
    <h3 class="mx-title" id="fw-title">Ferment Club workshops</h3>
    <p class="mx-text">Pick a pace. Tick the box below and a second question appears, with no script.</p>
    <fieldset class="mx-set">
      <legend>How often?</legend>
      <div class="btn-group mx-seg">
        <input class="btn-check" type="radio" name="freq" value="weekly" id="fw-f1" checked><label class="btn" for="fw-f1">Weekly</label>
        <input class="btn-check" type="radio" name="freq" value="monthly" id="fw-f2"><label class="btn" for="fw-f2">Monthly</label>
      </div>
    </fieldset>
    <div class="mx-field mx-day">
      <label class="form-label" for="fw-day">Which morning? (optional)</label>
      <select class="form-select" id="fw-day" name="day"><option>Monday</option><option selected>Thursday</option><option>Saturday</option></select>
    </div>
    <div class="mx-field">
      <label class="form-label" for="fw-email">Email address</label>
      <input class="form-control" id="fw-email" name="email" type="email" autocomplete="email" required aria-describedby="fw-email-msg">
      <p class="form-text" id="fw-email-msg">Enter an email address, like [email protected].</p>
    </div>
    <div class="mx-field">
      <div class="form-check"><input class="form-check-input mx-reveal" id="fw-local" name="local" type="checkbox"><label class="form-check-label" for="fw-local">Also tell me about workshops near me (optional)</label></div>
      <div class="mx-more">
        <div class="mx-field">
          <label class="form-label" for="fw-city">Nearest city (optional)</label>
          <select class="form-select" id="fw-city" name="city"><option value="">Choose a city</option><option>Bristol</option><option>Leeds</option><option>Glasgow</option><option>Norwich</option></select>
        </div>
      </div>
    </div>
    <button class="btn mx-btn" type="submit">Subscribe</button>
  </form>
  <div class="mx-done" id="fw-done" tabindex="-1" hidden><svg aria-hidden="true"><use href="#mx-i-tick"/></svg><div><b>Saved</b><p>Check <span data-email>your inbox</span> to confirm. Workshop dates come with the next issue.</p></div></div>
</div></section>
</div>
</div>

<div>
  <span class="lbl">Popover slide-in</span>
  <div class="demo-row"><button class="btn mx-open" type="button" popovertarget="mx-pop"><svg aria-hidden="true"><use href="#mx-i-mail"/></svg>Open the slide-in</button><span>Esc or a click outside closes it. No JavaScript.</span></div>
  <div class="stage" data-preview="mx-pop" aria-hidden="true" inert></div>
</div>

<div>
  <span class="lbl">Container query card</span>
  <div class="demo-cq">
    <div><small>In a 280px sidebar</small>
      <div class="mx-cq-wrap"><section class="card mx mx-cq" aria-labelledby="cq1-title"><div class="card-body">
        <div class="mx-cq-art" aria-hidden="true"><svg><use href="#mx-i-jar"/></svg></div>
        <div><h3 class="mx-title" id="cq1-title">The Brine Times</h3><p class="mx-text">Seasonal pickles, one recipe a month.</p>
          <form action="#" method="post" data-nf="cq1-done">
            <label class="form-label" for="cq1-email">Email address</label>
            <div class="mx-row"><input class="form-control" id="cq1-email" name="email" type="email" autocomplete="email" required><button class="btn mx-btn" type="submit">Subscribe</button></div>
          </form>
          <div class="mx-done" id="cq1-done" tabindex="-1" hidden><svg aria-hidden="true"><use href="#mx-i-tick"/></svg><div><b>Thanks</b><p>Confirm <span data-email>your address</span> to start.</p></div></div>
        </div>
      </div></section></div>
    </div>
    <div><small>In the main column</small>
      <div class="mx-cq-wrap"><section class="card mx mx-cq" aria-labelledby="cq2-title"><div class="card-body">
        <div class="mx-cq-art" aria-hidden="true"><svg><use href="#mx-i-jar"/></svg></div>
        <div><h3 class="mx-title" id="cq2-title">The Brine Times</h3><p class="mx-text">Seasonal pickles, one recipe a month.</p>
          <form action="#" method="post" data-nf="cq2-done">
            <label class="form-label" for="cq2-email">Email address</label>
            <div class="mx-row"><input class="form-control" id="cq2-email" name="email" type="email" autocomplete="email" required><button class="btn mx-btn" type="submit">Subscribe</button></div>
          </form>
          <div class="mx-done" id="cq2-done" tabindex="-1" hidden><svg aria-hidden="true"><use href="#mx-i-tick"/></svg><div><b>Thanks</b><p>Confirm <span data-email>your address</span> to start.</p></div></div>
        </div>
      </div></section></div>
    </div>
  </div>
</div>

<!-- the popover itself: opened by popovertarget, closed by Esc, a click outside or the close button -->
<div class="card mx mx-pop" id="mx-pop" popover role="dialog" aria-labelledby="mx-pop-title"><div class="card-body">
  <button class="btn-close" type="button" popovertarget="mx-pop" popovertargetaction="hide" aria-label="Close"></button>
  <h3 class="mx-title" id="mx-pop-title">Before you go back to the recipe</h3>
  <p class="mx-text">Get the next Ferment Club letter: a one-jar sauerkraut that's ready in a week.</p>
  <form action="#" method="post" data-nf="mx-pop-done">
    <div class="mx-field">
      <label class="form-label" for="mx-pop-email">Email address</label>
      <input class="form-control" id="mx-pop-email" name="email" type="email" autocomplete="email" required aria-describedby="mx-pop-msg">
      <p class="form-text" id="mx-pop-msg">Enter an email address, like [email protected].</p>
    </div>
    <button class="btn mx-btn" type="submit">Send me the next letter</button>
  </form>
  <div class="mx-done" id="mx-pop-done" tabindex="-1" hidden><svg aria-hidden="true"><use href="#mx-i-tick"/></svg><div><b>On its way</b><p>Confirm <span data-email>your address</span> and the sauerkraut letter is yours.</p></div></div>
</div></div>
CSS
/* Bootstrap .card, .form-control, .form-select, .form-check, .btn-check + .btn-group, .btn and .btn-close, with modern CSS
   doing the work Bootstrap would hand to JavaScript: :user-invalid / :user-valid for validation after interaction,
   :has() for the reveal and the ready button, a native popover for the slide-in, and a container query for the card. */
.mx{--mx-ink:#10231c;--mx-muted:#465a52;--mx-line:#738880;--mx-soft:#d6e2dc;--mx-accent:#047857;--mx-accent-dark:#065f46;--mx-ring:rgba(4,120,87,.22);--mx-tint:#e6f4ee;
  --mx-error:#b42318;--mx-idle:#d3ddd8;--mx-idle-ink:#2c4038;
  --bs-card-border-width:0;--bs-card-border-radius:20px;--bs-card-spacer-x:28px;--bs-card-spacer-y:28px;
  color:var(--mx-ink);font-size:.92rem;line-height:1.5;box-shadow:0 1px 2px rgba(16,35,28,.06),0 20px 44px -26px rgba(16,35,28,.4)}
.mx p,.mx h3,.mx legend{margin:0}
.mx a{color:var(--mx-accent-dark);font-weight:600;text-underline-offset:2px}
.mx a:focus-visible{outline:2px solid var(--mx-accent);outline-offset:2px;border-radius:3px}
.mx-title{font-size:1.3rem;font-weight:750;letter-spacing:-.02em;line-height:1.25}
.mx .mx-text{color:var(--mx-muted);margin:4px 0 18px}
.mx-field{margin-bottom:14px;min-width:0}
.mx .form-label{display:block;font-size:.84rem;font-weight:650;margin-bottom:6px}
.mx .form-control,.mx .form-select{min-width:0;font-size:inherit;line-height:inherit;color:var(--mx-ink);border:1.5px solid var(--mx-line);border-radius:10px;padding:10px 40px 10px 13px;
  background-repeat:no-repeat;background-position:right 12px center;background-size:18px;transition:border-color .15s,box-shadow .15s}
.mx .form-select{padding-right:36px;cursor:pointer;background-size:16px;
  --bs-form-select-bg-img:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23465a52' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m7 10 5 5 5-5'/%3E%3C/svg%3E")}
.mx .form-control:focus,.mx .form-select:focus{border-color:var(--mx-accent);box-shadow:0 0 0 4px var(--mx-ring)}
.mx .form-control:user-valid{border-color:var(--mx-accent);
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M5 12.5l4.5 4.5L19 7.5' fill='none' stroke='%23047857' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E")}
.mx .form-control:user-invalid{border-color:var(--mx-error)}
.mx .form-control:user-invalid:focus{box-shadow:0 0 0 4px rgba(180,35,24,.18)}
.mx .form-text{display:flex;gap:6px;align-items:flex-start;font-size:.8rem;color:var(--mx-muted);margin-top:6px}
.mx .mx-field:has(:user-invalid) .form-text{color:var(--mx-error);font-weight:600}
.mx .mx-field:has(:user-invalid) .form-text::before{content:"!";flex:none;display:grid;place-items:center;width:16px;height:16px;margin-top:1px;border-radius:50%;background:var(--mx-error);color:#fff;font-size:.7rem;font-weight:800}
.mx .form-check{display:flex;gap:10px;align-items:flex-start;font-size:.86rem;padding:0;margin:0;min-height:0}
.mx .form-check-input{float:none;flex:none;width:18px;height:18px;margin:2px 0 0;border:1.5px solid var(--mx-line);border-radius:5px;background-size:12px;cursor:pointer;transition:background-color .15s,border-color .15s}
.mx .form-check-input:checked{background-color:var(--mx-accent);border-color:var(--mx-accent)}
.mx .form-check-input:user-invalid{border-color:var(--mx-error)}
.mx .form-check-input:focus{box-shadow:none}
.mx .form-check-input:focus-visible{outline:2px solid var(--mx-accent);outline-offset:2px}
.mx .form-check-label{cursor:pointer}
.mx .form-check+.form-text{margin-left:28px}
.mx .mx-btn{--bs-btn-color:var(--mx-idle-ink);--bs-btn-bg:var(--mx-idle);--bs-btn-border-color:var(--mx-idle);--bs-btn-hover-color:var(--mx-idle-ink);--bs-btn-hover-bg:var(--mx-idle);
  --bs-btn-hover-border-color:var(--mx-idle);--bs-btn-active-color:var(--mx-idle-ink);--bs-btn-active-bg:var(--mx-idle);--bs-btn-active-border-color:var(--mx-idle);
  --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:16px;--bs-btn-padding-y:11px;
  --bs-btn-border-width:0;--bs-btn-border-radius:10px;display:block;width:100%;transition:background-color .2s,color .2s}
.mx form:not(:has(:invalid)) .mx-btn,.mx-cq .mx-btn{--bs-btn-color:#fff;--bs-btn-bg:var(--mx-accent);--bs-btn-hover-color:#fff;--bs-btn-hover-bg:var(--mx-accent-dark);
  --bs-btn-active-color:#fff;--bs-btn-active-bg:var(--mx-accent-dark)}
.mx .mx-btn:focus-visible{outline:2px solid var(--mx-accent);outline-offset:3px}
.mx .mx-done{outline:none;display:flex;gap:12px;align-items:flex-start;padding:14px 16px;border-radius:12px;background:var(--mx-tint)}
.mx .mx-done[hidden],.mx form[hidden]{display:none}
.mx .mx-done:focus-visible{outline:2px solid var(--mx-accent);outline-offset:3px}
.mx-done svg{flex:none;width:24px;height:24px;color:var(--mx-accent)}
.mx-done b{display:block}
.mx .mx-done p{color:var(--mx-muted);font-size:.86rem}
.mx-done span[data-email]{color:var(--mx-ink);font-weight:600;overflow-wrap:anywhere}

/* 2. :has() reveal */
.mx .mx-seg{display:grid;grid-template-columns:1fr 1fr;gap:4px;padding:4px;border:1px solid var(--mx-soft);border-radius:12px;background:#f2f7f4}
.mx .mx-seg .btn{--bs-btn-color:var(--mx-muted);--bs-btn-hover-color:var(--mx-ink);--bs-btn-active-color:var(--mx-ink);--bs-btn-active-bg:#fff;--bs-btn-active-border-color:transparent;
  --bs-btn-font-size:.86rem;--bs-btn-font-weight:600;--bs-btn-line-height:1.5;--bs-btn-padding-x:8px;--bs-btn-padding-y:8px;--bs-btn-border-width:0;--bs-btn-focus-box-shadow:none;
  border-radius:9px!important;margin:0!important;transition:background-color .15s,color .15s,box-shadow .15s}
.mx .mx-seg .btn-check:checked+.btn{box-shadow:0 1px 3px rgba(16,35,28,.18)}
.mx .mx-seg .btn-check:focus-visible+.btn{outline:2px solid var(--mx-accent);outline-offset:1px}
.mx-set{min-width:0;margin-bottom:14px}
.mx .mx-set legend{float:none;width:auto;font-size:.84rem;font-weight:650;line-height:1.5;padding:0;margin-bottom:6px}
.mx-more{display:none;margin:12px 0 0 28px;padding:12px 14px;border-left:3px solid var(--mx-accent);border-radius:0 10px 10px 0;background:#f4f9f6}
.mx form:has(.mx-reveal:checked) .mx-more{display:block}
.mx-more .mx-field{margin:0}
.mx-day{display:none}
.mx form:has([value=weekly]:checked) .mx-day{display:block}

/* 3. Popover slide-in */
.mx-open{--bs-btn-color:#fff;--bs-btn-bg:#047857;--bs-btn-border-color:#047857;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#065f46;--bs-btn-hover-border-color:#065f46;
  --bs-btn-active-color:#fff;--bs-btn-active-bg:#065f46;--bs-btn-active-border-color:#065f46;--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}
.mx-open:focus-visible{outline:2px solid #047857;outline-offset:3px}
.mx-open svg{width:18px;height:18px}
.mx-pop{position:fixed;inset:auto 20px 20px auto;width:min(360px,calc(100% - 24px));margin:0;border:0;padding:0;
  opacity:0;translate:0 24px;transition:opacity .25s,translate .25s,display .25s allow-discrete,overlay .25s allow-discrete}
.mx-pop:popover-open{opacity:1;translate:0 0}
.card.mx-pop:not(:popover-open){display:none}
@starting-style{.mx-pop:popover-open{opacity:0;translate:0 24px}}
.mx-pop .card-body{padding:22px 22px 20px}
.mx-pop .mx-title{font-size:1.1rem;padding-right:34px}
.mx-pop .mx-text{margin-bottom:14px}
.mx .btn-close{--bs-btn-close-focus-shadow:none;--bs-btn-close-opacity:.75;position:absolute;top:10px;right:10px;width:18px;height:18px;padding:8px;border-radius:9px;background-size:13px}
.mx .btn-close:hover{background-color:#eef3f0}
.mx .btn-close:focus-visible{outline:2px solid var(--mx-accent);outline-offset:1px}

/* 4. Container query card */
.mx-cq-wrap{container-type:inline-size}
.mx-cq .card-body{display:grid;gap:16px;padding:22px}
.mx-cq-art{display:grid;place-items:center;aspect-ratio:16/9;border-radius:14px;color:#fff;
  background:radial-gradient(circle at 18% 24%,#fbbf24 0 13%,transparent 14%),radial-gradient(circle at 84% 78%,#f97316 0 11%,transparent 12%),linear-gradient(135deg,#065f46,#047857 55%,#10b981)}
.mx-cq-art svg{width:40px;height:40px;opacity:.9}
.mx-cq .mx-text{margin-bottom:12px}
.mx-cq .mx-row{display:grid;gap:8px}
@container (min-width:520px){
  .mx-cq .card-body{grid-template-columns:170px 1fr;align-items:center;gap:22px}
  .mx-cq-art{aspect-ratio:1}
  .mx-cq .mx-row{grid-template-columns:1fr auto}
  .mx-cq .mx-btn{width:auto}
}

@media (max-width:420px){.mx{--bs-card-spacer-x:18px;--bs-card-spacer-y:22px}.mx-cq .card-body{padding:18px}.mx-pop{inset:auto 12px 12px 12px;width:auto}}
@media (prefers-reduced-motion:reduce){.mx .form-control,.mx .form-select,.mx .form-check-input,.mx .mx-btn,.mx .mx-seg .btn,.mx-pop{transition:none}}
JavaScript
// The only script: stop the demo submit and show the success message. The browser does all the validation, so the
// submit event only fires once every field is valid. Replace the body with your fetch() call.
document.querySelectorAll('form[data-nf]').forEach(function (form) {
  form.addEventListener('submit', function (e) {
    e.preventDefault();
    var done = document.getElementById(form.getAttribute('data-nf'));
    done.querySelector('[data-email]').textContent = form.elements.email.value;
    form.hidden = true; done.hidden = false; done.focus();
  });
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <symbol id="mx-i-tick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M8 12.5l2.8 2.8L16.5 9.5"/></symbol>
  <symbol id="mx-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="mx-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="mx-i-jar" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3h8v3H8zM7 6h10a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2zM5 12h14"/></symbol>
</svg>

<div class="grid grid-cols-[repeat(auto-fit,minmax(300px,1fr))] items-start gap-[34px]">
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#4c5f57] uppercase">:user-invalid validation</span>
<section class="rounded-[20px] bg-white p-7 text-[.92rem] leading-normal shadow-[0_1px_2px_rgba(16,35,28,.06),0_20px_44px_-26px_rgba(16,35,28,.4)] max-[420px]:px-[18px] max-[420px]:py-[22px]" aria-labelledby="fc-title">
  <form class="group/form" action="#" method="post" data-nf="fc-done">
    <h3 class="text-[1.3rem] leading-[1.25] font-[750] tracking-[-.02em]" id="fc-title">Ferment Club letters</h3>
    <p class="mt-1 mb-[18px] text-mx-muted">A fortnightly email on kimchi, kefir, miso and the science of letting food get on with it.</p>
    <div class="group mb-3.5 min-w-0">
      <label class="mb-1.5 block text-[.84rem] font-[650]" for="fc-email">Email address</label>
      <input class="block w-full min-w-0 rounded-[10px] border-[1.5px] border-mx-line bg-white bg-size-[18px] bg-position-[right_12px_center] bg-no-repeat py-2.5 pr-10 pl-[13px] text-mx-ink transition-[border-color,box-shadow] duration-150 focus:border-mx-accent focus:shadow-[0_0_0_4px_rgba(4,120,87,.22)] focus:outline-none user-valid:border-mx-accent user-valid:bg-mx-ok user-invalid:border-mx-error user-invalid:focus:shadow-[0_0_0_4px_rgba(180,35,24,.18)] motion-reduce:transition-none" id="fc-email" name="email" type="email" autocomplete="email" required aria-describedby="fc-email-msg">
      <p class="mt-1.5 flex items-start gap-1.5 text-[.8rem] text-mx-muted group-has-user-invalid:font-semibold group-has-user-invalid:text-mx-error group-has-user-invalid:before:mt-px group-has-user-invalid:before:grid group-has-user-invalid:before:size-4 group-has-user-invalid:before:flex-none group-has-user-invalid:before:place-items-center group-has-user-invalid:before:rounded-full group-has-user-invalid:before:bg-mx-error group-has-user-invalid:before:text-[.7rem] group-has-user-invalid:before:font-extrabold group-has-user-invalid:before:text-white group-has-user-invalid:before:content-['!']" id="fc-email-msg">Enter an email address, like [email protected].</p>
    </div>
    <div class="group mb-3.5 min-w-0">
      <label class="flex cursor-pointer items-start gap-2.5 text-[.86rem]"><input class="mt-0.5 size-[18px] flex-none cursor-pointer appearance-none rounded-[5px] border-[1.5px] border-mx-line bg-white bg-size-[12px] bg-center bg-no-repeat transition-colors duration-150 checked:border-mx-accent checked:bg-mx-accent checked:bg-mx-tick user-invalid:border-mx-error focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-mx-accent motion-reduce:transition-none" id="fc-consent" name="consent" type="checkbox" required aria-describedby="fc-consent-msg"><span>I agree to receive Ferment Club emails. I can unsubscribe at any time.</span></label>
      <p class="mt-1.5 ml-7 flex items-start gap-1.5 text-[.8rem] text-mx-muted group-has-user-invalid:font-semibold group-has-user-invalid:text-mx-error group-has-user-invalid:before:mt-px group-has-user-invalid:before:grid group-has-user-invalid:before:size-4 group-has-user-invalid:before:flex-none group-has-user-invalid:before:place-items-center group-has-user-invalid:before:rounded-full group-has-user-invalid:before:bg-mx-error group-has-user-invalid:before:text-[.7rem] group-has-user-invalid:before:font-extrabold group-has-user-invalid:before:text-white group-has-user-invalid:before:content-['!']" id="fc-consent-msg"><span>Required, so we know you asked. <a class="font-semibold text-mx-accent-dark underline underline-offset-2 focus-visible:rounded-[3px] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-mx-accent" href="#privacy">Privacy notice</a></span></p>
    </div>
    <button class="block w-full cursor-pointer rounded-[10px] bg-mx-accent px-4 py-[11px] font-[650] text-white transition-colors duration-200 hover:bg-mx-accent-dark focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-mx-accent group-has-invalid/form:bg-mx-idle group-has-invalid/form:text-mx-idle-ink motion-reduce:transition-none" type="submit">Subscribe</button>
  </form>
  <div class="flex items-start gap-3 rounded-xl bg-mx-tint px-4 py-3.5 focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-mx-accent" id="fc-done" tabindex="-1" hidden><svg class="size-6 flex-none text-mx-accent" aria-hidden="true"><use href="#mx-i-tick"/></svg><div><b class="block">Almost there</b><p class="text-[.86rem] text-mx-muted">Confirm <span class="font-semibold wrap-anywhere text-mx-ink" data-email>your address</span> from the email we just sent.</p></div></div>
</section>
</div>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#4c5f57] uppercase">:has() reveals extra fields</span>
<section class="rounded-[20px] bg-white p-7 text-[.92rem] leading-normal shadow-[0_1px_2px_rgba(16,35,28,.06),0_20px_44px_-26px_rgba(16,35,28,.4)] max-[420px]:px-[18px] max-[420px]:py-[22px]" aria-labelledby="fw-title">
  <form class="group/form" action="#" method="post" data-nf="fw-done">
    <h3 class="text-[1.3rem] leading-[1.25] font-[750] tracking-[-.02em]" id="fw-title">Ferment Club workshops</h3>
    <p class="mt-1 mb-[18px] text-mx-muted">Pick a pace. Tick the box below and a second question appears, with no script.</p>
    <fieldset class="mb-3.5 min-w-0">
      <legend class="mb-1.5 text-[.84rem] font-[650]">How often?</legend>
      <div class="grid grid-cols-2 gap-1 rounded-xl border border-mx-soft bg-[#f2f7f4] p-1">
        <label class="relative cursor-pointer rounded-[9px] p-2 text-center text-[.86rem] font-semibold text-mx-muted transition-[background-color,color,box-shadow] duration-150 has-checked:bg-white has-checked:text-mx-ink has-checked:shadow-[0_1px_3px_rgba(16,35,28,.18)] has-focus-visible:outline-2 has-focus-visible:outline-offset-1 has-focus-visible:outline-mx-accent motion-reduce:transition-none"><input class="weekly sr-only" type="radio" name="freq" value="weekly" checked>Weekly</label>
        <label class="relative cursor-pointer rounded-[9px] p-2 text-center text-[.86rem] font-semibold text-mx-muted transition-[background-color,color,box-shadow] duration-150 has-checked:bg-white has-checked:text-mx-ink has-checked:shadow-[0_1px_3px_rgba(16,35,28,.18)] has-focus-visible:outline-2 has-focus-visible:outline-offset-1 has-focus-visible:outline-mx-accent motion-reduce:transition-none"><input class="sr-only" type="radio" name="freq" value="monthly">Monthly</label>
      </div>
    </fieldset>
    <div class="mb-3.5 hidden min-w-0 group-has-[.weekly:checked]/form:block">
      <label class="mb-1.5 block text-[.84rem] font-[650]" for="fw-day">Which morning? (optional)</label>
      <select class="block w-full cursor-pointer appearance-none rounded-[10px] border-[1.5px] border-mx-line bg-white bg-mx-chevron bg-size-[16px] bg-position-[right_12px_center] bg-no-repeat py-2.5 pr-9 pl-[13px] text-mx-ink transition-[border-color,box-shadow] duration-150 focus:border-mx-accent focus:shadow-[0_0_0_4px_rgba(4,120,87,.22)] focus:outline-none motion-reduce:transition-none" id="fw-day" name="day"><option>Monday</option><option selected>Thursday</option><option>Saturday</option></select>
    </div>
    <div class="group mb-3.5 min-w-0">
      <label class="mb-1.5 block text-[.84rem] font-[650]" for="fw-email">Email address</label>
      <input class="block w-full min-w-0 rounded-[10px] border-[1.5px] border-mx-line bg-white bg-size-[18px] bg-position-[right_12px_center] bg-no-repeat py-2.5 pr-10 pl-[13px] text-mx-ink transition-[border-color,box-shadow] duration-150 focus:border-mx-accent focus:shadow-[0_0_0_4px_rgba(4,120,87,.22)] focus:outline-none user-valid:border-mx-accent user-valid:bg-mx-ok user-invalid:border-mx-error user-invalid:focus:shadow-[0_0_0_4px_rgba(180,35,24,.18)] motion-reduce:transition-none" id="fw-email" name="email" type="email" autocomplete="email" required aria-describedby="fw-email-msg">
      <p class="mt-1.5 flex items-start gap-1.5 text-[.8rem] text-mx-muted group-has-user-invalid:font-semibold group-has-user-invalid:text-mx-error group-has-user-invalid:before:mt-px group-has-user-invalid:before:grid group-has-user-invalid:before:size-4 group-has-user-invalid:before:flex-none group-has-user-invalid:before:place-items-center group-has-user-invalid:before:rounded-full group-has-user-invalid:before:bg-mx-error group-has-user-invalid:before:text-[.7rem] group-has-user-invalid:before:font-extrabold group-has-user-invalid:before:text-white group-has-user-invalid:before:content-['!']" id="fw-email-msg">Enter an email address, like [email protected].</p>
    </div>
    <div class="mb-3.5 min-w-0">
      <label class="flex cursor-pointer items-start gap-2.5 text-[.86rem]"><input class="reveal mt-0.5 size-[18px] flex-none cursor-pointer appearance-none rounded-[5px] border-[1.5px] border-mx-line bg-white bg-size-[12px] bg-center bg-no-repeat transition-colors duration-150 checked:border-mx-accent checked:bg-mx-accent checked:bg-mx-tick focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-mx-accent motion-reduce:transition-none" name="local" type="checkbox"><span>Also tell me about workshops near me (optional)</span></label>
      <div class="mt-3 ml-7 hidden rounded-r-[10px] border-l-3 border-mx-accent bg-[#f4f9f6] px-3.5 py-3 group-has-[.reveal:checked]/form:block">
        <label class="mb-1.5 block text-[.84rem] font-[650]" for="fw-city">Nearest city (optional)</label>
        <select class="block w-full cursor-pointer appearance-none rounded-[10px] border-[1.5px] border-mx-line bg-white bg-mx-chevron bg-size-[16px] bg-position-[right_12px_center] bg-no-repeat py-2.5 pr-9 pl-[13px] text-mx-ink transition-[border-color,box-shadow] duration-150 focus:border-mx-accent focus:shadow-[0_0_0_4px_rgba(4,120,87,.22)] focus:outline-none motion-reduce:transition-none" id="fw-city" name="city"><option value="">Choose a city</option><option>Bristol</option><option>Leeds</option><option>Glasgow</option><option>Norwich</option></select>
      </div>
    </div>
    <button class="block w-full cursor-pointer rounded-[10px] bg-mx-accent px-4 py-[11px] font-[650] text-white transition-colors duration-200 hover:bg-mx-accent-dark focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-mx-accent group-has-invalid/form:bg-mx-idle group-has-invalid/form:text-mx-idle-ink motion-reduce:transition-none" type="submit">Subscribe</button>
  </form>
  <div class="flex items-start gap-3 rounded-xl bg-mx-tint px-4 py-3.5 focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-mx-accent" id="fw-done" tabindex="-1" hidden><svg class="size-6 flex-none text-mx-accent" aria-hidden="true"><use href="#mx-i-tick"/></svg><div><b class="block">Saved</b><p class="text-[.86rem] text-mx-muted">Check <span class="font-semibold wrap-anywhere text-mx-ink" data-email>your inbox</span> to confirm. Workshop dates come with the next issue.</p></div></div>
</section>
</div>
</div>

<div>
  <span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#4c5f57] uppercase">Popover slide-in</span>
  <div class="mb-4 flex flex-wrap items-center gap-x-4 gap-y-2.5 text-[.86rem] text-[#4c5f57]"><button class="inline-flex cursor-pointer items-center gap-2 rounded-[10px] bg-mx-accent px-4 py-2.5 text-[.9rem] font-[650] text-white transition-colors duration-150 hover:bg-mx-accent-dark focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-mx-accent motion-reduce:transition-none" type="button" popovertarget="mx-pop"><svg class="size-[18px]" aria-hidden="true"><use href="#mx-i-mail"/></svg>Open the slide-in</button><span>Esc or a click outside closes it. No JavaScript.</span></div>
  <div class="stage" data-preview="mx-pop" aria-hidden="true" inert></div>
</div>

<div>
  <span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#4c5f57] uppercase">Container query card</span>
  <div class="grid grid-cols-[280px_1fr] items-start gap-6 max-[700px]:grid-cols-1">
    <div><small class="mb-2 block text-[.76rem] text-[#4c5f57]">In a 280px sidebar</small>
      <div class="@container"><section class="grid gap-4 rounded-[20px] bg-white p-[22px] text-[.92rem] leading-normal shadow-[0_1px_2px_rgba(16,35,28,.06),0_20px_44px_-26px_rgba(16,35,28,.4)] @min-[520px]:grid-cols-[170px_1fr] @min-[520px]:items-center @min-[520px]:gap-[22px] max-[420px]:p-[18px]" aria-labelledby="cq1-title">
        <div class="grid aspect-video place-items-center rounded-[14px] bg-mx-art text-white @min-[520px]:aspect-square" aria-hidden="true"><svg class="size-10 opacity-90"><use href="#mx-i-jar"/></svg></div>
        <div><h3 class="text-[1.3rem] leading-[1.25] font-[750] tracking-[-.02em]" id="cq1-title">The Brine Times</h3><p class="mt-1 mb-3 text-mx-muted">Seasonal pickles, one recipe a month.</p>
          <form action="#" method="post" data-nf="cq1-done">
            <label class="mb-1.5 block text-[.84rem] font-[650]" for="cq1-email">Email address</label>
            <div class="grid gap-2 @min-[520px]:grid-cols-[1fr_auto]"><input class="block w-full min-w-0 rounded-[10px] border-[1.5px] border-mx-line bg-white bg-size-[18px] bg-position-[right_12px_center] bg-no-repeat py-2.5 pr-10 pl-[13px] text-mx-ink transition-[border-color,box-shadow] duration-150 focus:border-mx-accent focus:shadow-[0_0_0_4px_rgba(4,120,87,.22)] focus:outline-none user-valid:border-mx-accent user-valid:bg-mx-ok user-invalid:border-mx-error motion-reduce:transition-none" id="cq1-email" name="email" type="email" autocomplete="email" required><button class="block w-full cursor-pointer rounded-[10px] bg-mx-accent px-4 py-[11px] font-[650] text-white transition-colors duration-200 hover:bg-mx-accent-dark focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-mx-accent motion-reduce:transition-none @min-[520px]:w-auto" type="submit">Subscribe</button></div>
          </form>
          <div class="flex items-start gap-3 rounded-xl bg-mx-tint px-4 py-3.5 focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-mx-accent" id="cq1-done" tabindex="-1" hidden><svg class="size-6 flex-none text-mx-accent" aria-hidden="true"><use href="#mx-i-tick"/></svg><div><b class="block">Thanks</b><p class="text-[.86rem] text-mx-muted">Confirm <span class="font-semibold wrap-anywhere text-mx-ink" data-email>your address</span> to start.</p></div></div>
        </div>
      </section></div>
    </div>
    <div><small class="mb-2 block text-[.76rem] text-[#4c5f57]">In the main column</small>
      <div class="@container"><section class="grid gap-4 rounded-[20px] bg-white p-[22px] text-[.92rem] leading-normal shadow-[0_1px_2px_rgba(16,35,28,.06),0_20px_44px_-26px_rgba(16,35,28,.4)] @min-[520px]:grid-cols-[170px_1fr] @min-[520px]:items-center @min-[520px]:gap-[22px] max-[420px]:p-[18px]" aria-labelledby="cq2-title">
        <div class="grid aspect-video place-items-center rounded-[14px] bg-mx-art text-white @min-[520px]:aspect-square" aria-hidden="true"><svg class="size-10 opacity-90"><use href="#mx-i-jar"/></svg></div>
        <div><h3 class="text-[1.3rem] leading-[1.25] font-[750] tracking-[-.02em]" id="cq2-title">The Brine Times</h3><p class="mt-1 mb-3 text-mx-muted">Seasonal pickles, one recipe a month.</p>
          <form action="#" method="post" data-nf="cq2-done">
            <label class="mb-1.5 block text-[.84rem] font-[650]" for="cq2-email">Email address</label>
            <div class="grid gap-2 @min-[520px]:grid-cols-[1fr_auto]"><input class="block w-full min-w-0 rounded-[10px] border-[1.5px] border-mx-line bg-white bg-size-[18px] bg-position-[right_12px_center] bg-no-repeat py-2.5 pr-10 pl-[13px] text-mx-ink transition-[border-color,box-shadow] duration-150 focus:border-mx-accent focus:shadow-[0_0_0_4px_rgba(4,120,87,.22)] focus:outline-none user-valid:border-mx-accent user-valid:bg-mx-ok user-invalid:border-mx-error motion-reduce:transition-none" id="cq2-email" name="email" type="email" autocomplete="email" required><button class="block w-full cursor-pointer rounded-[10px] bg-mx-accent px-4 py-[11px] font-[650] text-white transition-colors duration-200 hover:bg-mx-accent-dark focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-mx-accent motion-reduce:transition-none @min-[520px]:w-auto" type="submit">Subscribe</button></div>
          </form>
          <div class="flex items-start gap-3 rounded-xl bg-mx-tint px-4 py-3.5 focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-mx-accent" id="cq2-done" tabindex="-1" hidden><svg class="size-6 flex-none text-mx-accent" aria-hidden="true"><use href="#mx-i-tick"/></svg><div><b class="block">Thanks</b><p class="text-[.86rem] text-mx-muted">Confirm <span class="font-semibold wrap-anywhere text-mx-ink" data-email>your address</span> to start.</p></div></div>
        </div>
      </section></div>
    </div>
  </div>
</div>

<!-- the popover itself: opened by popovertarget, closed by Esc, a click outside or the close button -->
<div class="fixed inset-auto right-5 bottom-5 m-0 w-[min(360px,calc(100%-24px))] translate-y-6 rounded-[20px] border-0 bg-white px-[22px] pt-[22px] pb-5 text-[.92rem] leading-normal text-mx-ink opacity-0 shadow-[0_1px_2px_rgba(16,35,28,.06),0_20px_44px_-26px_rgba(16,35,28,.4)] transition-[opacity,translate,display,overlay] transition-discrete duration-250 open:translate-y-0 open:opacity-100 starting:open:translate-y-6 starting:open:opacity-0 motion-reduce:transition-none max-[420px]:right-3 max-[420px]:bottom-3 max-[420px]:left-3 max-[420px]:w-auto" id="mx-pop" popover role="dialog" aria-labelledby="mx-pop-title">
  <button class="absolute top-2.5 right-2.5 grid size-[34px] cursor-pointer place-items-center rounded-[9px] text-mx-muted hover:bg-[#eef3f0] hover:text-mx-ink focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-mx-accent" type="button" popovertarget="mx-pop" popovertargetaction="hide" aria-label="Close"><svg class="size-5" aria-hidden="true"><use href="#mx-i-x"/></svg></button>
  <h3 class="pr-[34px] text-[1.1rem] leading-[1.25] font-[750] tracking-[-.02em]" id="mx-pop-title">Before you go back to the recipe</h3>
  <p class="mt-1 mb-3.5 text-mx-muted">Get the next Ferment Club letter: a one-jar sauerkraut that's ready in a week.</p>
  <form class="group/form" action="#" method="post" data-nf="mx-pop-done">
    <div class="group mb-3.5 min-w-0">
      <label class="mb-1.5 block text-[.84rem] font-[650]" for="mx-pop-email">Email address</label>
      <input class="block w-full min-w-0 rounded-[10px] border-[1.5px] border-mx-line bg-white bg-size-[18px] bg-position-[right_12px_center] bg-no-repeat py-2.5 pr-10 pl-[13px] text-mx-ink transition-[border-color,box-shadow] duration-150 focus:border-mx-accent focus:shadow-[0_0_0_4px_rgba(4,120,87,.22)] focus:outline-none user-valid:border-mx-accent user-valid:bg-mx-ok user-invalid:border-mx-error user-invalid:focus:shadow-[0_0_0_4px_rgba(180,35,24,.18)] motion-reduce:transition-none" id="mx-pop-email" name="email" type="email" autocomplete="email" required aria-describedby="mx-pop-msg">
      <p class="mt-1.5 flex items-start gap-1.5 text-[.8rem] text-mx-muted group-has-user-invalid:font-semibold group-has-user-invalid:text-mx-error group-has-user-invalid:before:mt-px group-has-user-invalid:before:grid group-has-user-invalid:before:size-4 group-has-user-invalid:before:flex-none group-has-user-invalid:before:place-items-center group-has-user-invalid:before:rounded-full group-has-user-invalid:before:bg-mx-error group-has-user-invalid:before:text-[.7rem] group-has-user-invalid:before:font-extrabold group-has-user-invalid:before:text-white group-has-user-invalid:before:content-['!']" id="mx-pop-msg">Enter an email address, like [email protected].</p>
    </div>
    <button class="block w-full cursor-pointer rounded-[10px] bg-mx-accent px-4 py-[11px] font-[650] text-white transition-colors duration-200 hover:bg-mx-accent-dark focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-mx-accent group-has-invalid/form:bg-mx-idle group-has-invalid/form:text-mx-idle-ink motion-reduce:transition-none" type="submit">Send me the next letter</button>
  </form>
  <div class="flex items-start gap-3 rounded-xl bg-mx-tint px-4 py-3.5 focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-mx-accent" id="mx-pop-done" tabindex="-1" hidden><svg class="size-6 flex-none text-mx-accent" aria-hidden="true"><use href="#mx-i-tick"/></svg><div><b class="block">On its way</b><p class="text-[.86rem] text-mx-muted">Confirm <span class="font-semibold wrap-anywhere text-mx-ink" data-email>your address</span> and the sauerkraut letter is yours.</p></div></div>
</div>
CSS
@theme {
  --color-mx-ink: #10231c;
  --color-mx-muted: #465a52;
  --color-mx-line: #738880;
  --color-mx-soft: #d6e2dc;
  --color-mx-accent: #047857;
  --color-mx-accent-dark: #065f46;
  --color-mx-tint: #e6f4ee;
  --color-mx-error: #b42318;
  --color-mx-idle: #d3ddd8;
  --color-mx-idle-ink: #2c4038;
  --background-image-mx-ok: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M5 12.5l4.5 4.5L19 7.5' fill='none' stroke='%23047857' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  --background-image-mx-tick: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8.5l3 3 6-7' fill='none' stroke='%23fff' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  --background-image-mx-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23465a52' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m7 10 5 5 5-5'/%3E%3C/svg%3E");
  --background-image-mx-art: radial-gradient(circle at 18% 24%, #fbbf24 0 13%, transparent 14%), radial-gradient(circle at 84% 78%, #f97316 0 11%, transparent 12%), linear-gradient(135deg, #065f46, #047857 55%, #10b981);
}
JavaScript
// The only script: stop the demo submit and show the success message. The browser does all the validation, so the
// submit event only fires once every field is valid. Replace the body with your fetch() call.
document.querySelectorAll('form[data-nf]').forEach(function (form) {
  form.addEventListener('submit', function (e) {
    e.preventDefault();
    var done = document.getElementById(form.getAttribute('data-nf'));
    done.querySelector('[data-email]').textContent = form.elements.email.value;
    form.hidden = true; done.hidden = false; done.focus();
  });
});

About this set

Four newsletter forms that use current CSS for what usually needs JavaScript. :user-invalid validation shows each field’s requirement as plain text, then turns it red with an exclamation mark only after someone has edited the field or tried to submit, while :user-valid adds a green tick; a required consent box behaves the same way. :has() reveals extra fields shows a weekday select only while Weekly is chosen and an optional city select once a box is ticked. In both forms the Subscribe button stays grey until the form is valid, but it is never disabled, so the browser still explains what is missing. Popover slide-in uses the popover attribute and popovertarget, so it opens, closes on Esc or a click outside, and slides in and out with @starting-style and transition-behavior, all without script. Container query card is one component shown in a 280 pixel sidebar and in the main column: it stacks when its container is narrow and goes side by side from 520 pixels. The only script stops the demo submit and shows the success message; nothing is sent.

What’s included

  • :user-invalid and :user-valid for errors and ticks after interaction
  • :has() for revealed fields and a button that looks ready when the form is valid
  • Popover slide-in with enter and exit transitions, no JavaScript
  • Container query layout that follows its container, not the viewport
  • Bootstrap and Tailwind versions with the same modern CSS

Accessibility

Every field has a visible label and a requirement message linked with aria-describedby, which stays in place and only changes colour when invalid. Revealed fields are optional, so hidden fields never block submitting. The popover is a role="dialog" named by its heading, with a labelled close button, and closes on Esc. The submit button is never disabled, focus rings are visible, and transitions switch off under prefers-reduced-motion.

Customise it

Change --mx-accent, --mx-accent-dark, --mx-idle and --mx-error on .mx. The container breakpoint is the 520px in the @container rule, and the slide-in timing is on .mx-pop. In Tailwind edit the mx-* colours and the @min-[520px] variants.