<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
<symbol id="nf-i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></symbol>
<symbol id="nf-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="nf-i-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 6l6 6-6 6"/></symbol>
<symbol id="nf-i-wave" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 15c2.5 0 2.5-2 5-2s2.5 2 5 2 2.5-2 5-2 2.5 2 5 2M2 19c2.5 0 2.5-2 5-2s2.5 2 5 2 2.5-2 5-2 2.5 2 5 2M12 10c-2-1-3-3-2-6 3 1 4 3 3 6"/></symbol>
</svg>
<div><span class="lbl">Joined bar with privacy note</span>
<section class="nf nf-card" aria-label="The Margin newsletter">
<form class="nf-form" action="#" method="post" data-nf="mg-done">
<label class="nf-label" for="mg-email">Get The Margin every Friday</label>
<p class="nf-sub">Notes from independent bookshops: new openings, staff picks and the occasional author interview.</p>
<div class="nf-join">
<input class="nf-input" id="mg-email" name="email" type="email" autocomplete="email" required placeholder="[email protected]"
aria-describedby="mg-email-err mg-note" data-err="mg-email-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="nf-btn" type="submit">Subscribe</button>
</div>
<p class="nf-err" id="mg-email-err"></p>
<p class="nf-note" id="mg-note"><svg aria-hidden="true"><use href="#nf-i-lock"/></svg><span>One email a week. We never share or sell your address. Read the <a href="#privacy">privacy notice</a>; unsubscribe from any email in one click.</span></p>
</form>
<div class="nf-done" id="mg-done" tabindex="-1" hidden>
<div class="nf-done-row">
<span class="nf-tick" aria-hidden="true"><svg><use href="#nf-i-tick"/></svg></span>
<div><h3>Check your inbox</h3>
<p>We sent a confirmation link to <b data-email>your address</b>. Click it and the next issue is yours.</p>
<button class="nf-link" type="button" data-nf-back>Use a different address</button></div>
</div>
</div>
</section>
</div>
<div><span class="lbl">Band with pitch beside the form</span>
<section class="nf nf-band" aria-labelledby="tp-title">
<div class="nf-pitch">
<span class="nf-mark" aria-hidden="true"><svg><use href="#nf-i-wave"/></svg></span>
<p class="nf-kicker">Free, every Sunday</p>
<h3 id="tp-title">Tidepool Weekly</h3>
<p>What turned up on the shore this week, which tides to catch, and one creature explained properly.</p>
</div>
<div>
<form class="nf-form" action="#" method="post" data-nf="tp-done">
<label class="nf-label" for="tp-email">Email address</label>
<div class="nf-stack">
<input class="nf-input" id="tp-email" name="email" type="email" autocomplete="email" required
aria-describedby="tp-email-err tp-note" data-err="tp-email-err"
data-missing="Enter your email address to get Tidepool Weekly." data-bad="Enter an email address with an @ and a domain, like [email protected].">
<button class="nf-btn" type="submit">Subscribe</button>
</div>
<p class="nf-err" id="tp-email-err"></p>
<p class="nf-note" id="tp-note"><span>Sent from Falmouth to 6,240 readers. Your address is only used for this newsletter. <a href="#privacy">Privacy</a></span></p>
</form>
<div class="nf-done" id="tp-done" tabindex="-1" hidden>
<div class="nf-done-row">
<span class="nf-tick" aria-hidden="true"><svg><use href="#nf-i-tick"/></svg></span>
<div><h3>Nearly there</h3><p>Confirm <b data-email>your address</b> from the email we just sent and Sunday's issue will find you.</p>
<button class="nf-link" type="button" data-nf-back>Use a different address</button></div>
</div>
</div>
</div>
</section>
</div>
<div><span class="lbl">Underline, minimal</span>
<section class="nf nf-line-form" aria-labelledby="sp-title">
<p class="nf-kicker">Slow Pour Letters</p>
<h3 id="sp-title">One coffee, brewed slowly, once a month</h3>
<p class="nf-sub">A single origin, a recipe and the farm behind it. Twelve emails a year.</p>
<form class="nf-form" action="#" method="post" data-nf="sp-done">
<label class="nf-label" for="sp-email">Your email</label>
<div class="nf-under">
<input class="nf-input" id="sp-email" name="email" type="email" autocomplete="email" required placeholder="[email protected]"
aria-describedby="sp-email-err sp-note" data-err="sp-email-err"
data-missing="Enter your email address to sign up." data-bad="Enter an email address with an @ and a domain, like [email protected].">
<button class="nf-btn" type="submit">Sign up <svg aria-hidden="true"><use href="#nf-i-arrow"/></svg></button>
</div>
<p class="nf-err" id="sp-email-err"></p>
<p class="nf-note" id="sp-note"><span>Monthly, never more. <a href="#privacy">How we use your email</a></span></p>
</form>
<div class="nf-done" id="sp-done" tabindex="-1" hidden>
<div class="nf-done-row">
<span class="nf-tick" aria-hidden="true"><svg><use href="#nf-i-tick"/></svg></span>
<div><h3>Thanks, one last step</h3><p>Open the email we sent to <b data-email>your address</b> and confirm. The next letter goes out on the 1st.</p>
<button class="nf-link" type="button" data-nf-back>Use a different address</button></div>
</div>
</div>
</section>
</div>
<div class="demo-grid">
<div><span class="lbl">Error state</span>
<section class="nf nf-card" aria-label="The Margin newsletter, error example">
<form class="nf-form" action="#" method="post" data-nf="mg2-done">
<label class="nf-label" for="mg2-email">Get The Margin every Friday</label>
<p class="nf-sub">Notes from independent bookshops.</p>
<div class="nf-join">
<input class="nf-input is-invalid" id="mg2-email" name="email" type="email" autocomplete="email" required value="maya@hollowbooks"
aria-invalid="true" aria-describedby="mg2-email-err mg2-note" data-err="mg2-email-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="nf-btn" type="submit">Subscribe</button>
</div>
<p class="nf-err" id="mg2-email-err">Enter an email address with an @ and a domain, like [email protected].</p>
<p class="nf-note" id="mg2-note"><svg aria-hidden="true"><use href="#nf-i-lock"/></svg><span>One email a week. Unsubscribe any time.</span></p>
</form>
<div class="nf-done" id="mg2-done" tabindex="-1" hidden>
<div class="nf-done-row">
<span class="nf-tick" aria-hidden="true"><svg><use href="#nf-i-tick"/></svg></span>
<div><h3>Check your inbox</h3><p>We sent a confirmation link to <b data-email>your address</b>.</p>
<button class="nf-link" type="button" data-nf-back>Use a different address</button></div>
</div>
</div>
</section>
</div>
<div><span class="lbl">Success state</span>
<section class="nf nf-card" aria-label="The Margin newsletter, success example">
<div class="nf-done" tabindex="-1">
<div class="nf-done-row">
<span class="nf-tick" aria-hidden="true"><svg><use href="#nf-i-tick"/></svg></span>
<div><h3>Check your inbox</h3>
<p>We sent a confirmation link to <b>[email protected]</b>. Click it and the next issue is yours.</p>
<p>Nothing there after five minutes? Look in your spam or promotions folder.</p></div>
</div>
</div>
</section>
</div>
</div>
CSS
/* Inline subscribe bars: a visible label, one email field, one button and a privacy note.
The note and the error message are both tied to the input with aria-describedby, so a screen reader
hears them with the field. Errors appear only after a submit attempt; the success state replaces the form. */
.nf{--nf-ink:#1f1b16;--nf-muted:#5c544a;--nf-line:#8a7f72;--nf-accent:#a63d1c;--nf-accent-dark:#84301a;--nf-accent-ink:#fff;
--nf-ring:rgba(166,61,28,.24);--nf-error:#b42318;--nf-ok:#1d6b3a;--nf-paper:#fff;--nf-serif:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;
color:var(--nf-ink);font-size:.94rem;line-height:1.5}
.nf a{color:inherit;text-decoration:underline;text-underline-offset:2px}
.nf a:focus-visible{outline:2px solid var(--nf-accent);outline-offset:2px;border-radius:3px}
.nf-label{display:block;font-weight:650;margin-bottom:8px}
.nf-input{display:block;width:100%;min-width:0;font:inherit;color:var(--nf-ink);background:var(--nf-paper);border:0;border-radius:9px;padding:11px 14px}
.nf-input::placeholder{color:#766c60;opacity:1}
.nf-input:focus{outline:none}
.nf-btn{flex:none;display:inline-flex;align-items:center;justify-content:center;gap:8px;font:inherit;font-weight:650;color:var(--nf-accent-ink);background:var(--nf-accent);
border:0;border-radius:9px;padding:11px 20px;cursor:pointer;transition:background-color .15s,transform .15s}
.nf-btn:hover{background:var(--nf-accent-dark)}
.nf-btn:active{transform:translateY(1px)}
.nf-btn:focus-visible{outline:2px solid var(--nf-accent);outline-offset:3px}
.nf-btn svg{width:18px;height:18px}
/* the joined field: border and focus ring live on the wrapper so input and button read as one control */
.nf-join{display:flex;gap:6px;padding:5px;background:var(--nf-paper);border:1.5px solid var(--nf-line);border-radius:13px;transition:border-color .15s,box-shadow .15s}
.nf-join:focus-within{border-color:var(--nf-accent);box-shadow:0 0 0 4px var(--nf-ring)}
.nf-join:has(.is-invalid){border-color:var(--nf-error)}
.nf-join:has(.is-invalid):focus-within{box-shadow:0 0 0 4px rgba(180,35,24,.18)}
.nf-err{display:flex;gap:7px;align-items:flex-start;font-size:.84rem;font-weight:600;color:var(--nf-error);margin-top:8px}
.nf-err:empty{display:none}
.nf-err::before{content:"!";flex:none;display:grid;place-items:center;width:17px;height:17px;margin-top:1px;border-radius:50%;background:var(--nf-error);color:#fff;font-size:.72rem;font-weight:800}
.nf-note{display:flex;gap:8px;align-items:flex-start;font-size:.82rem;color:var(--nf-muted);margin-top:10px}
.nf-note svg{flex:none;width:15px;height:15px;margin-top:2px}
.nf-done{outline:none}
.nf-done[hidden],.nf-form[hidden]{display:none}
.nf-done:focus-visible{outline:2px solid var(--nf-accent);outline-offset:6px;border-radius:10px}
.nf-done-row{display:flex;gap:14px;align-items:flex-start}
.nf-tick{flex:none;display:grid;place-items:center;width:40px;height:40px;border-radius:50%;background:#dcefe2;color:var(--nf-ok)}
.nf-tick svg{width:20px;height:20px}
.nf-done h3{font-family:var(--nf-serif);font-size:1.2rem;font-weight:700;line-height:1.25}
.nf-done p{color:var(--nf-muted);margin-top:3px}
.nf-done b{color:var(--nf-ink);overflow-wrap:anywhere}
.nf-link{font:inherit;font-size:.86rem;font-weight:600;color:var(--nf-accent-dark);background:none;border:0;padding:0;margin-top:8px;cursor:pointer;
text-decoration:underline;text-underline-offset:2px}
.nf-link:focus-visible{outline:2px solid var(--nf-accent);outline-offset:2px;border-radius:3px}
/* 1. Joined bar with privacy note */
.nf-card{background:var(--nf-paper);border:1px solid #e3dacd;border-radius:18px;padding:26px 28px;box-shadow:0 1px 2px rgba(31,27,22,.05),0 18px 40px -26px rgba(31,27,22,.35)}
.nf-card .nf-label{font-family:var(--nf-serif);font-size:1.25rem;font-weight:700;letter-spacing:-.01em;margin-bottom:4px}
.nf-card .nf-sub{color:var(--nf-muted);margin-bottom:14px}
/* 2. Band: pitch on one side, form on the other */
.nf-band{--nf-accent:#0f5c55;--nf-accent-dark:#0a433e;--nf-ring:rgba(15,92,85,.24);--nf-muted:#3b5450;--nf-line:#6f8b86;
display:grid;grid-template-columns:1fr 1.05fr;gap:30px;align-items:center;background:#dcebe6;border-radius:20px;padding:30px 32px}
.nf-band .nf-mark{display:grid;place-items:center;width:44px;height:44px;border-radius:12px;background:#0f5c55;color:#fff;margin-bottom:14px}
.nf-band .nf-mark svg{width:24px;height:24px}
.nf-kicker{font-size:.74rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:#0f5c55}
.nf-band h3{font-family:var(--nf-serif);font-size:1.5rem;font-weight:700;line-height:1.2;letter-spacing:-.01em;margin:3px 0 6px}
.nf-band .nf-pitch p{color:var(--nf-muted)}
.nf-stack{display:flex;gap:8px}
.nf-stack .nf-input{flex:1 1 0;border:1.5px solid var(--nf-line);padding:11px 14px;transition:border-color .15s,box-shadow .15s}
.nf-stack .nf-input:focus{border-color:var(--nf-accent);box-shadow:0 0 0 4px var(--nf-ring)}
.nf-stack .nf-input.is-invalid{border-color:var(--nf-error)}
/* 3. Underline, minimal */
.nf-line-form{max-width:520px;margin:0 auto;text-align:center}
.nf-line-form .nf-kicker{color:var(--nf-accent-dark)}
.nf-line-form h3{font-family:var(--nf-serif);font-size:1.6rem;font-weight:700;line-height:1.2;margin:4px 0 6px}
.nf-line-form .nf-sub{color:var(--nf-muted);margin-bottom:20px}
.nf-line-form .nf-form{text-align:left}
.nf-under{display:flex;align-items:center;gap:10px;border-bottom:2px solid var(--nf-ink);transition:border-color .15s,box-shadow .15s}
.nf-under:focus-within{border-color:var(--nf-accent);box-shadow:0 2px 0 var(--nf-accent)}
.nf-under:has(.is-invalid){border-color:var(--nf-error)}
.nf-under .nf-input{background:transparent;border-radius:0;padding:10px 2px;font-size:1.05rem}
.nf-under .nf-btn{color:var(--nf-accent-dark);background:transparent;padding:8px 6px;border-radius:8px}
.nf-under .nf-btn:hover{background:rgba(166,61,28,.1)}
.nf-under .nf-btn svg{transition:transform .15s}
.nf-under .nf-btn:hover svg{transform:translateX(3px)}
.nf-line-form .nf-done{text-align:left}
@media (max-width:640px){.nf-band{grid-template-columns:1fr;gap:20px;padding:26px 22px}}
@media (max-width:420px){.nf-card{padding:22px 18px}.nf-stack{flex-direction:column}.nf-join .nf-btn{padding:10px 14px}}
@media (prefers-reduced-motion:reduce){.nf-btn,.nf-join,.nf-stack .nf-input,.nf-under,.nf-under .nf-btn svg{transition:none}}
JavaScript
// Front-end demo only: nothing is sent. On submit the script checks each field that has data-err, writes the message
// into the element the input already points to with aria-describedby, and moves focus to the first problem.
// A valid submit swaps the form for its success state and focuses it. Replace that branch with your fetch() call.
// Without JavaScript the browser validates the form itself and posts it to the action URL.
(function () {
function check(el) {
var msg = '';
if (el.tagName === 'FIELDSET') { if (!el.querySelector('input:checked')) msg = el.getAttribute('data-missing'); }
else if (!el.checkValidity()) msg = 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 (el.tagName !== 'FIELDSET') { 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();
var first = null;
form.querySelectorAll('[data-err]').forEach(function (el) { if (!check(el) && !first) first = el; });
if (first) { (first.tagName === 'FIELDSET' ? first.querySelector('input') : first).focus(); return; }
if (!done) return;
done.querySelectorAll('[data-email]').forEach(function (n) { n.textContent = email.value; });
form.hidden = true; done.hidden = false; done.focus();
});
// once a field shows an error, re-check it while it is being fixed
['input', 'change'].forEach(function (type) {
form.addEventListener(type, function (e) { var el = e.target.closest('.is-invalid'); if (el) check(el); });
});
var back = done && done.querySelector('[data-nf-back]');
if (back) back.addEventListener('click', function () { done.hidden = true; form.hidden = false; email.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="nf-i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></symbol>
<symbol id="nf-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="nf-i-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 6l6 6-6 6"/></symbol>
<symbol id="nf-i-wave" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 15c2.5 0 2.5-2 5-2s2.5 2 5 2 2.5-2 5-2 2.5 2 5 2M2 19c2.5 0 2.5-2 5-2s2.5 2 5 2 2.5-2 5-2 2.5 2 5 2M12 10c-2-1-3-3-2-6 3 1 4 3 3 6"/></symbol>
</svg>
<div><span class="lbl">Joined bar with privacy note</span>
<section class="card nf nf-card" aria-label="The Margin newsletter">
<form action="#" method="post" data-nf="mg-done">
<label class="form-label nf-title" for="mg-email">Get The Margin every Friday</label>
<p class="nf-sub">Notes from independent bookshops: new openings, staff picks and the occasional author interview.</p>
<div class="input-group nf-join">
<input class="form-control" id="mg-email" name="email" type="email" autocomplete="email" required placeholder="[email protected]"
aria-describedby="mg-email-err mg-note" data-err="mg-email-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 nf-btn" type="submit">Subscribe</button>
</div>
<p class="nf-err" id="mg-email-err"></p>
<p class="form-text" id="mg-note"><svg aria-hidden="true"><use href="#nf-i-lock"/></svg><span>One email a week. We never share or sell your address. Read the <a href="#privacy">privacy notice</a>; unsubscribe from any email in one click.</span></p>
</form>
<div class="nf-done" id="mg-done" tabindex="-1" hidden>
<div class="d-flex gap-3 align-items-start">
<span class="nf-tick" aria-hidden="true"><svg><use href="#nf-i-tick"/></svg></span>
<div><h3>Check your inbox</h3>
<p>We sent a confirmation link to <b data-email>your address</b>. Click it and the next issue is yours.</p>
<button class="btn btn-link nf-link" type="button" data-nf-back>Use a different address</button></div>
</div>
</div>
</section>
</div>
<div><span class="lbl">Band with pitch beside the form</span>
<section class="nf nf-band" aria-labelledby="tp-title">
<div class="nf-pitch">
<span class="nf-mark" aria-hidden="true"><svg><use href="#nf-i-wave"/></svg></span>
<p class="nf-kicker">Free, every Sunday</p>
<h3 id="tp-title">Tidepool Weekly</h3>
<p>What turned up on the shore this week, which tides to catch, and one creature explained properly.</p>
</div>
<div>
<form action="#" method="post" data-nf="tp-done">
<label class="form-label" for="tp-email">Email address</label>
<div class="d-flex gap-2 nf-stack">
<input class="form-control" id="tp-email" name="email" type="email" autocomplete="email" required
aria-describedby="tp-email-err tp-note" data-err="tp-email-err"
data-missing="Enter your email address to get Tidepool Weekly." data-bad="Enter an email address with an @ and a domain, like [email protected].">
<button class="btn nf-btn" type="submit">Subscribe</button>
</div>
<p class="nf-err" id="tp-email-err"></p>
<p class="form-text" id="tp-note"><span>Sent from Falmouth to 6,240 readers. Your address is only used for this newsletter. <a href="#privacy">Privacy</a></span></p>
</form>
<div class="nf-done" id="tp-done" tabindex="-1" hidden>
<div class="d-flex gap-3 align-items-start">
<span class="nf-tick" aria-hidden="true"><svg><use href="#nf-i-tick"/></svg></span>
<div><h3>Nearly there</h3><p>Confirm <b data-email>your address</b> from the email we just sent and Sunday's issue will find you.</p>
<button class="btn btn-link nf-link" type="button" data-nf-back>Use a different address</button></div>
</div>
</div>
</div>
</section>
</div>
<div><span class="lbl">Underline, minimal</span>
<section class="nf nf-line-form" aria-labelledby="sp-title">
<p class="nf-kicker">Slow Pour Letters</p>
<h3 id="sp-title">One coffee, brewed slowly, once a month</h3>
<p class="nf-sub">A single origin, a recipe and the farm behind it. Twelve emails a year.</p>
<form action="#" method="post" data-nf="sp-done">
<label class="form-label" for="sp-email">Your email</label>
<div class="input-group nf-under">
<input class="form-control" id="sp-email" name="email" type="email" autocomplete="email" required placeholder="[email protected]"
aria-describedby="sp-email-err sp-note" data-err="sp-email-err"
data-missing="Enter your email address to sign up." data-bad="Enter an email address with an @ and a domain, like [email protected].">
<button class="btn nf-btn" type="submit">Sign up <svg aria-hidden="true"><use href="#nf-i-arrow"/></svg></button>
</div>
<p class="nf-err" id="sp-email-err"></p>
<p class="form-text" id="sp-note"><span>Monthly, never more. <a href="#privacy">How we use your email</a></span></p>
</form>
<div class="nf-done" id="sp-done" tabindex="-1" hidden>
<div class="d-flex gap-3 align-items-start">
<span class="nf-tick" aria-hidden="true"><svg><use href="#nf-i-tick"/></svg></span>
<div><h3>Thanks, one last step</h3><p>Open the email we sent to <b data-email>your address</b> and confirm. The next letter goes out on the 1st.</p>
<button class="btn btn-link nf-link" type="button" data-nf-back>Use a different address</button></div>
</div>
</div>
</section>
</div>
<div class="demo-grid">
<div><span class="lbl">Error state</span>
<section class="card nf nf-card" aria-label="The Margin newsletter, error example">
<form action="#" method="post" data-nf="mg2-done">
<label class="form-label nf-title" for="mg2-email">Get The Margin every Friday</label>
<p class="nf-sub">Notes from independent bookshops.</p>
<div class="input-group nf-join">
<input class="form-control is-invalid" id="mg2-email" name="email" type="email" autocomplete="email" required value="maya@hollowbooks"
aria-invalid="true" aria-describedby="mg2-email-err mg2-note" data-err="mg2-email-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 nf-btn" type="submit">Subscribe</button>
</div>
<p class="nf-err" id="mg2-email-err">Enter an email address with an @ and a domain, like [email protected].</p>
<p class="form-text" id="mg2-note"><svg aria-hidden="true"><use href="#nf-i-lock"/></svg><span>One email a week. Unsubscribe any time.</span></p>
</form>
<div class="nf-done" id="mg2-done" tabindex="-1" hidden>
<div class="d-flex gap-3 align-items-start">
<span class="nf-tick" aria-hidden="true"><svg><use href="#nf-i-tick"/></svg></span>
<div><h3>Check your inbox</h3><p>We sent a confirmation link to <b data-email>your address</b>.</p>
<button class="btn btn-link nf-link" type="button" data-nf-back>Use a different address</button></div>
</div>
</div>
</section>
</div>
<div><span class="lbl">Success state</span>
<section class="card nf nf-card" aria-label="The Margin newsletter, success example">
<div class="nf-done" tabindex="-1">
<div class="d-flex gap-3 align-items-start">
<span class="nf-tick" aria-hidden="true"><svg><use href="#nf-i-tick"/></svg></span>
<div><h3>Check your inbox</h3>
<p>We sent a confirmation link to <b>[email protected]</b>. Click it and the next issue is yours.</p>
<p>Nothing there after five minutes? Look in your spam or promotions folder.</p></div>
</div>
</div>
</section>
</div>
</div>
// Front-end demo only: nothing is sent. On submit the script checks each field that has data-err, writes the message
// into the element the input already points to with aria-describedby, and moves focus to the first problem.
// A valid submit swaps the form for its success state and focuses it. Replace that branch with your fetch() call.
// Without JavaScript the browser validates the form itself and posts it to the action URL.
(function () {
function check(el) {
var msg = '';
if (el.tagName === 'FIELDSET') { if (!el.querySelector('input:checked')) msg = el.getAttribute('data-missing'); }
else if (!el.checkValidity()) msg = 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 (el.tagName !== 'FIELDSET') { 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();
var first = null;
form.querySelectorAll('[data-err]').forEach(function (el) { if (!check(el) && !first) first = el; });
if (first) { (first.tagName === 'FIELDSET' ? first.querySelector('input') : first).focus(); return; }
if (!done) return;
done.querySelectorAll('[data-email]').forEach(function (n) { n.textContent = email.value; });
form.hidden = true; done.hidden = false; done.focus();
});
// once a field shows an error, re-check it while it is being fixed
['input', 'change'].forEach(function (type) {
form.addEventListener(type, function (e) { var el = e.target.closest('.is-invalid'); if (el) check(el); });
});
var back = done && done.querySelector('[data-nf-back]');
if (back) back.addEventListener('click', function () { done.hidden = true; form.hidden = false; email.focus(); });
});
})();
Tailwind 4 code
Requires: Tailwind CSS 4
HTML
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
<symbol id="nf-i-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></symbol>
<symbol id="nf-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="nf-i-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 6l6 6-6 6"/></symbol>
<symbol id="nf-i-wave" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 15c2.5 0 2.5-2 5-2s2.5 2 5 2 2.5-2 5-2 2.5 2 5 2M2 19c2.5 0 2.5-2 5-2s2.5 2 5 2 2.5-2 5-2 2.5 2 5 2M12 10c-2-1-3-3-2-6 3 1 4 3 3 6"/></symbol>
</svg>
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#635a4f] uppercase">Joined bar with privacy note</span>
<section class="rounded-[18px] border border-[#e3dacd] bg-white px-7 py-[26px] text-[.94rem] leading-normal shadow-[0_1px_2px_rgba(31,27,22,.05),0_18px_40px_-26px_rgba(31,27,22,.35)] max-[420px]:px-[18px] max-[420px]:py-[22px]" aria-label="The Margin newsletter">
<form action="#" method="post" data-nf="mg-done">
<label class="mb-1 block font-nf-serif text-[1.25rem] font-bold tracking-[-.01em]" for="mg-email">Get The Margin every Friday</label>
<p class="mb-3.5 text-nf-muted">Notes from independent bookshops: new openings, staff picks and the occasional author interview.</p>
<div class="flex gap-1.5 rounded-[13px] border-[1.5px] border-nf-line bg-white p-[5px] transition-[border-color,box-shadow] duration-150 focus-within:border-nf-accent focus-within:shadow-[0_0_0_4px_rgba(166,61,28,.24)] has-[.is-invalid]:border-nf-error motion-reduce:transition-none">
<input class="block w-full min-w-0 rounded-[9px] bg-white px-3.5 py-[11px] text-nf-ink placeholder:text-[#766c60] focus:outline-none" id="mg-email" name="email" type="email" autocomplete="email" required placeholder="[email protected]"
aria-describedby="mg-email-err mg-note" data-err="mg-email-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="inline-flex flex-none cursor-pointer items-center justify-center gap-2 rounded-[9px] bg-nf-accent px-5 py-[11px] font-[650] text-white transition-[background-color,translate] duration-150 hover:bg-nf-accent-dark active:translate-y-px focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-nf-accent motion-reduce:transition-none max-[420px]:px-3.5 max-[420px]:py-2.5" type="submit">Subscribe</button>
</div>
<p class="mt-2 flex items-start gap-[7px] text-[.84rem] font-semibold text-nf-error empty:hidden before:mt-px before:grid before:size-[17px] before:flex-none before:place-items-center before:rounded-full before:bg-nf-error before:text-[.72rem] before:font-extrabold before:text-white before:content-['!']" id="mg-email-err"></p>
<p class="mt-2.5 flex items-start gap-2 text-[.82rem] text-nf-muted" id="mg-note"><svg class="mt-0.5 size-[15px] flex-none" aria-hidden="true"><use href="#nf-i-lock"/></svg><span>One email a week. We never share or sell your address. Read the <a class="underline underline-offset-2 focus-visible:rounded-[3px] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nf-accent" href="#privacy">privacy notice</a>; unsubscribe from any email in one click.</span></p>
</form>
<div class="focus-visible:rounded-[10px] focus-visible:outline-2 focus-visible:outline-offset-6 focus-visible:outline-nf-accent" id="mg-done" tabindex="-1" hidden>
<div class="flex items-start gap-3.5">
<span class="grid size-10 flex-none place-items-center rounded-full bg-[#dcefe2] text-nf-ok" aria-hidden="true"><svg class="size-5"><use href="#nf-i-tick"/></svg></span>
<div><h3 class="font-nf-serif text-[1.2rem] leading-tight font-bold">Check your inbox</h3>
<p class="mt-[3px] text-nf-muted">We sent a confirmation link to <b class="text-nf-ink wrap-anywhere" data-email>your address</b>. Click it and the next issue is yours.</p>
<button class="mt-2 cursor-pointer text-[.86rem] font-semibold text-nf-accent-dark underline underline-offset-2 focus-visible:rounded-[3px] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nf-accent" type="button" data-nf-back>Use a different address</button></div>
</div>
</div>
</section>
</div>
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#635a4f] uppercase">Band with pitch beside the form</span>
<section class="grid grid-cols-[1fr_1.05fr] items-center gap-[30px] rounded-[20px] bg-[#dcebe6] px-8 py-[30px] text-[.94rem] leading-normal max-[640px]:grid-cols-1 max-[640px]:gap-5 max-[640px]:px-[22px] max-[640px]:py-[26px]" aria-labelledby="tp-title">
<div>
<span class="mb-3.5 grid size-11 place-items-center rounded-xl bg-nf-teal text-white" aria-hidden="true"><svg class="size-6"><use href="#nf-i-wave"/></svg></span>
<p class="text-[.74rem] font-bold tracking-[.1em] text-nf-teal uppercase">Free, every Sunday</p>
<h3 class="mt-[3px] mb-1.5 font-nf-serif text-[1.5rem] leading-[1.2] font-bold tracking-[-.01em]" id="tp-title">Tidepool Weekly</h3>
<p class="text-nf-teal-muted">What turned up on the shore this week, which tides to catch, and one creature explained properly.</p>
</div>
<div>
<form action="#" method="post" data-nf="tp-done">
<label class="mb-2 block font-[650]" for="tp-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-[9px] border-[1.5px] border-nf-teal-line bg-white px-3.5 py-[11px] text-nf-ink transition-[border-color,box-shadow] duration-150 focus:border-nf-teal focus:shadow-[0_0_0_4px_rgba(15,92,85,.24)] focus:outline-none [&.is-invalid]:border-nf-error motion-reduce:transition-none max-[420px]:basis-auto" id="tp-email" name="email" type="email" autocomplete="email" required
aria-describedby="tp-email-err tp-note" data-err="tp-email-err"
data-missing="Enter your email address to get Tidepool Weekly." data-bad="Enter an email address with an @ and a domain, like [email protected].">
<button class="inline-flex flex-none cursor-pointer items-center justify-center gap-2 rounded-[9px] bg-nf-teal px-5 py-[11px] font-[650] text-white transition-[background-color,translate] duration-150 hover:bg-nf-teal-dark active:translate-y-px focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-nf-teal motion-reduce:transition-none" type="submit">Subscribe</button>
</div>
<p class="mt-2 flex items-start gap-[7px] text-[.84rem] font-semibold text-nf-error empty:hidden before:mt-px before:grid before:size-[17px] before:flex-none before:place-items-center before:rounded-full before:bg-nf-error before:text-[.72rem] before:font-extrabold before:text-white before:content-['!']" id="tp-email-err"></p>
<p class="mt-2.5 text-[.82rem] text-nf-teal-muted" id="tp-note">Sent from Falmouth to 6,240 readers. Your address is only used for this newsletter. <a class="underline underline-offset-2 focus-visible:rounded-[3px] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nf-teal" href="#privacy">Privacy</a></p>
</form>
<div class="focus-visible:rounded-[10px] focus-visible:outline-2 focus-visible:outline-offset-6 focus-visible:outline-nf-teal" id="tp-done" tabindex="-1" hidden>
<div class="flex items-start gap-3.5">
<span class="grid size-10 flex-none place-items-center rounded-full bg-[#dcefe2] text-nf-ok" aria-hidden="true"><svg class="size-5"><use href="#nf-i-tick"/></svg></span>
<div><h3 class="font-nf-serif text-[1.2rem] leading-tight font-bold">Nearly there</h3><p class="mt-[3px] text-nf-teal-muted">Confirm <b class="text-nf-ink wrap-anywhere" data-email>your address</b> from the email we just sent and Sunday's issue will find you.</p>
<button class="mt-2 cursor-pointer text-[.86rem] font-semibold text-nf-teal-dark underline underline-offset-2 focus-visible:rounded-[3px] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nf-teal" type="button" data-nf-back>Use a different address</button></div>
</div>
</div>
</div>
</section>
</div>
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#635a4f] uppercase">Underline, minimal</span>
<section class="mx-auto max-w-[520px] text-center text-[.94rem] leading-normal" aria-labelledby="sp-title">
<p class="text-[.74rem] font-bold tracking-[.1em] text-nf-accent-dark uppercase">Slow Pour Letters</p>
<h3 class="mt-1 mb-1.5 font-nf-serif text-[1.6rem] leading-[1.2] font-bold" id="sp-title">One coffee, brewed slowly, once a month</h3>
<p class="mb-5 text-nf-muted">A single origin, a recipe and the farm behind it. Twelve emails a year.</p>
<form class="text-left" action="#" method="post" data-nf="sp-done">
<label class="mb-2 block font-[650]" for="sp-email">Your email</label>
<div class="flex items-center gap-2.5 border-b-2 border-nf-ink transition-[border-color,box-shadow] duration-150 focus-within:border-nf-accent focus-within:shadow-[0_2px_0_var(--color-nf-accent)] has-[.is-invalid]:border-nf-error motion-reduce:transition-none">
<input class="block w-full min-w-0 bg-transparent px-0.5 py-2.5 text-[1.05rem] text-nf-ink placeholder:text-[#766c60] focus:outline-none" id="sp-email" name="email" type="email" autocomplete="email" required placeholder="[email protected]"
aria-describedby="sp-email-err sp-note" data-err="sp-email-err"
data-missing="Enter your email address to sign up." data-bad="Enter an email address with an @ and a domain, like [email protected].">
<button class="group inline-flex flex-none cursor-pointer items-center gap-2 rounded-lg px-1.5 py-2 font-[650] text-nf-accent-dark transition-colors duration-150 hover:bg-[rgba(166,61,28,.1)] focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-nf-accent motion-reduce:transition-none" type="submit">Sign up <svg class="size-[18px] transition-transform duration-150 group-hover:translate-x-[3px] motion-reduce:transition-none" aria-hidden="true"><use href="#nf-i-arrow"/></svg></button>
</div>
<p class="mt-2 flex items-start gap-[7px] text-[.84rem] font-semibold text-nf-error empty:hidden before:mt-px before:grid before:size-[17px] before:flex-none before:place-items-center before:rounded-full before:bg-nf-error before:text-[.72rem] before:font-extrabold before:text-white before:content-['!']" id="sp-email-err"></p>
<p class="mt-2.5 text-[.82rem] text-nf-muted" id="sp-note">Monthly, never more. <a class="underline underline-offset-2 focus-visible:rounded-[3px] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nf-accent" href="#privacy">How we use your email</a></p>
</form>
<div class="text-left focus-visible:rounded-[10px] focus-visible:outline-2 focus-visible:outline-offset-6 focus-visible:outline-nf-accent" id="sp-done" tabindex="-1" hidden>
<div class="flex items-start gap-3.5">
<span class="grid size-10 flex-none place-items-center rounded-full bg-[#dcefe2] text-nf-ok" aria-hidden="true"><svg class="size-5"><use href="#nf-i-tick"/></svg></span>
<div><h3 class="font-nf-serif text-[1.2rem] leading-tight font-bold">Thanks, one last step</h3><p class="mt-[3px] text-nf-muted">Open the email we sent to <b class="text-nf-ink wrap-anywhere" data-email>your address</b> and confirm. The next letter goes out on the 1st.</p>
<button class="mt-2 cursor-pointer text-[.86rem] font-semibold text-nf-accent-dark underline underline-offset-2 focus-visible:rounded-[3px] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nf-accent" type="button" data-nf-back>Use a different address</button></div>
</div>
</div>
</section>
</div>
<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-[#635a4f] uppercase">Error state</span>
<section class="rounded-[18px] border border-[#e3dacd] bg-white px-7 py-[26px] text-[.94rem] leading-normal shadow-[0_1px_2px_rgba(31,27,22,.05),0_18px_40px_-26px_rgba(31,27,22,.35)] max-[420px]:px-[18px] max-[420px]:py-[22px]" aria-label="The Margin newsletter, error example">
<form action="#" method="post" data-nf="mg2-done">
<label class="mb-1 block font-nf-serif text-[1.25rem] font-bold tracking-[-.01em]" for="mg2-email">Get The Margin every Friday</label>
<p class="mb-3.5 text-nf-muted">Notes from independent bookshops.</p>
<div class="flex gap-1.5 rounded-[13px] border-[1.5px] border-nf-line bg-white p-[5px] transition-[border-color,box-shadow] duration-150 focus-within:border-nf-accent focus-within:shadow-[0_0_0_4px_rgba(166,61,28,.24)] has-[.is-invalid]:border-nf-error motion-reduce:transition-none">
<input class="is-invalid block w-full min-w-0 rounded-[9px] bg-white px-3.5 py-[11px] text-nf-ink placeholder:text-[#766c60] focus:outline-none" id="mg2-email" name="email" type="email" autocomplete="email" required value="maya@hollowbooks"
aria-invalid="true" aria-describedby="mg2-email-err mg2-note" data-err="mg2-email-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="inline-flex flex-none cursor-pointer items-center justify-center gap-2 rounded-[9px] bg-nf-accent px-5 py-[11px] font-[650] text-white transition-[background-color,translate] duration-150 hover:bg-nf-accent-dark active:translate-y-px focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-nf-accent motion-reduce:transition-none max-[420px]:px-3.5 max-[420px]:py-2.5" type="submit">Subscribe</button>
</div>
<p class="mt-2 flex items-start gap-[7px] text-[.84rem] font-semibold text-nf-error empty:hidden before:mt-px before:grid before:size-[17px] before:flex-none before:place-items-center before:rounded-full before:bg-nf-error before:text-[.72rem] before:font-extrabold before:text-white before:content-['!']" id="mg2-email-err">Enter an email address with an @ and a domain, like [email protected].</p>
<p class="mt-2.5 flex items-start gap-2 text-[.82rem] text-nf-muted" id="mg2-note"><svg class="mt-0.5 size-[15px] flex-none" aria-hidden="true"><use href="#nf-i-lock"/></svg><span>One email a week. Unsubscribe any time.</span></p>
</form>
<div class="focus-visible:rounded-[10px] focus-visible:outline-2 focus-visible:outline-offset-6 focus-visible:outline-nf-accent" id="mg2-done" tabindex="-1" hidden>
<div class="flex items-start gap-3.5">
<span class="grid size-10 flex-none place-items-center rounded-full bg-[#dcefe2] text-nf-ok" aria-hidden="true"><svg class="size-5"><use href="#nf-i-tick"/></svg></span>
<div><h3 class="font-nf-serif text-[1.2rem] leading-tight font-bold">Check your inbox</h3><p class="mt-[3px] text-nf-muted">We sent a confirmation link to <b class="text-nf-ink wrap-anywhere" data-email>your address</b>.</p>
<button class="mt-2 cursor-pointer text-[.86rem] font-semibold text-nf-accent-dark underline underline-offset-2 focus-visible:rounded-[3px] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nf-accent" type="button" data-nf-back>Use a different address</button></div>
</div>
</div>
</section>
</div>
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#635a4f] uppercase">Success state</span>
<section class="rounded-[18px] border border-[#e3dacd] bg-white px-7 py-[26px] text-[.94rem] leading-normal shadow-[0_1px_2px_rgba(31,27,22,.05),0_18px_40px_-26px_rgba(31,27,22,.35)] max-[420px]:px-[18px] max-[420px]:py-[22px]" aria-label="The Margin newsletter, success example">
<div class="focus-visible:rounded-[10px] focus-visible:outline-2 focus-visible:outline-offset-6 focus-visible:outline-nf-accent" tabindex="-1">
<div class="flex items-start gap-3.5">
<span class="grid size-10 flex-none place-items-center rounded-full bg-[#dcefe2] text-nf-ok" aria-hidden="true"><svg class="size-5"><use href="#nf-i-tick"/></svg></span>
<div><h3 class="font-nf-serif text-[1.2rem] leading-tight font-bold">Check your inbox</h3>
<p class="mt-[3px] text-nf-muted">We sent a confirmation link to <b class="text-nf-ink wrap-anywhere">[email protected]</b>. Click it and the next issue is yours.</p>
<p class="mt-[3px] text-nf-muted">Nothing there after five minutes? Look in your spam or promotions folder.</p></div>
</div>
</div>
</section>
</div>
</div>
// Front-end demo only: nothing is sent. On submit the script checks each field that has data-err, writes the message
// into the element the input already points to with aria-describedby, and moves focus to the first problem.
// A valid submit swaps the form for its success state and focuses it. Replace that branch with your fetch() call.
// Without JavaScript the browser validates the form itself and posts it to the action URL.
(function () {
function check(el) {
var msg = '';
if (el.tagName === 'FIELDSET') { if (!el.querySelector('input:checked')) msg = el.getAttribute('data-missing'); }
else if (!el.checkValidity()) msg = 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 (el.tagName !== 'FIELDSET') { 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();
var first = null;
form.querySelectorAll('[data-err]').forEach(function (el) { if (!check(el) && !first) first = el; });
if (first) { (first.tagName === 'FIELDSET' ? first.querySelector('input') : first).focus(); return; }
if (!done) return;
done.querySelectorAll('[data-email]').forEach(function (n) { n.textContent = email.value; });
form.hidden = true; done.hidden = false; done.focus();
});
// once a field shows an error, re-check it while it is being fixed
['input', 'change'].forEach(function (type) {
form.addEventListener(type, function (e) { var el = e.target.closest('.is-invalid'); if (el) check(el); });
});
var back = done && done.querySelector('[data-nf-back]');
if (back) back.addEventListener('click', function () { done.hidden = true; form.hidden = false; email.focus(); });
});
})();
About this set
Three inline newsletter sign-ups plus their error and success states, for the place a subscribe form usually lives: the end of an article, a sidebar or a page footer. Joined bar with privacy note is a white card for an independent bookshop newsletter, with the email field and Subscribe button drawn as one bordered control and a lock icon beside the privacy line. Band with pitch beside the form puts the newsletter name, a short promise and a wave mark on the left and the form on the right, stacking on phones. Underline, minimal is a centred coffee letter with a bottom-border field and a text button with an arrow. The last two cards show the same bar in its error state, with the message tied to the field, and in its check your inbox state. Every field has a visible label and autocomplete=”email”. A short vanilla script switches the browser’s validation to inline messages: on submit it writes the error into the element the input already points to with aria-describedby, sets aria-invalid and moves focus there; a valid address swaps the form for a confirmation that names the address and offers to use a different one. Nothing is sent; the demo is front-end only.
What’s included
Three layouts: joined bar, two-column band, underline field
Static error and success examples alongside the live forms
Privacy note and error message linked to the input with aria-describedby
Success state names the address and offers to change it
Bootstrap version uses .input-group, .form-control, .form-text and .card
Accessibility
Each email input has a visible label, type="email" and autocomplete="email", and its privacy note and error element are referenced by aria-describedby. On a failed submit the input gets aria-invalid="true" and focus, so the message is read with the field; the success panel receives focus when it appears. Inputs, buttons and links show a visible focus ring, and transitions switch off under prefers-reduced-motion.
Customise it
Change --nf-accent, --nf-accent-dark and --nf-ring on .nf for the button and focus colour, --nf-line for field borders and --nf-error for errors; .nf-band overrides the accent with its own teal. In Bootstrap the same variables feed the .btn and .input-group; in Tailwind edit the nf-* colours in @theme.