<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
<symbol id="dk-i-tick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" 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="dk-i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.5l4.5 4.5L19 7.5"/></symbol>
<symbol id="dk-i-wave" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12h2M7 8v8M11 5v14M15 9v6M19 11v2M21 12h0"/></symbol>
</svg>
<div><span class="lbl">Developer newsletter bar</span>
<section class="dk dk-dev" aria-labelledby="st-title">
<div>
<p class="dk-mono">// issue #212 ships Friday</p>
<h3 id="st-title">Stack Trace Weekly</h3>
<p>Five links worth your time on databases, performance and the occasional war story. No hot takes.</p>
</div>
<div>
<form class="dk-form" action="#" method="post" data-nf="st-done">
<label class="dk-label" for="st-email">Email address</label>
<div class="dk-join">
<input class="dk-input" id="st-email" name="email" type="email" autocomplete="email" required placeholder="[email protected]" aria-describedby="st-email-err st-hint" data-err="st-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="dk-btn" type="submit">Subscribe</button>
</div>
<p class="dk-err" id="st-email-err"></p>
<p class="dk-hint" id="st-hint">Fridays only. Plain-text friendly. <a href="#privacy">Privacy</a></p>
</form>
<div class="dk-done" id="st-done" tabindex="-1" hidden>
<div class="dk-done-row"><svg aria-hidden="true"><use href="#dk-i-tick"/></svg><div><b>Check your inbox</b><p>Confirm <span data-email>your address</span> and issue #212 is yours on Friday.</p></div></div>
</div>
<p class="dk-stat"><span class="dk-dot" aria-hidden="true"></span>Read by 41,800 engineers</p>
</div>
</section>
</div>
<div class="demo-grid">
<div><span class="lbl">Star-field card with topics</span>
<section class="dk dk-sky" aria-labelledby="ns-title">
<div class="dk-sky-head"><span>New moon edition</span></div>
<div class="dk-sky-body">
<h3 id="ns-title">Nightshift</h3>
<p>What's worth looking up at this month, from a club of 300 amateur astronomers in the Peak District.</p>
<form class="dk-form" action="#" method="post" data-nf="ns-done">
<fieldset class="dk-set">
<legend>Topics (optional)</legend>
<div class="dk-chips">
<label class="dk-chip"><input type="checkbox" name="topics" value="events" checked><svg aria-hidden="true"><use href="#dk-i-check"/></svg>Dark-sky events</label>
<label class="dk-chip"><input type="checkbox" name="topics" value="gear"><svg aria-hidden="true"><use href="#dk-i-check"/></svg>Telescope reviews</label>
<label class="dk-chip"><input type="checkbox" name="topics" value="photo" checked><svg aria-hidden="true"><use href="#dk-i-check"/></svg>Astrophotography</label>
<label class="dk-chip"><input type="checkbox" name="topics" value="kids"><svg aria-hidden="true"><use href="#dk-i-check"/></svg>For kids</label>
</div>
</fieldset>
<label class="dk-label" for="ns-email">Email address</label>
<div class="dk-row">
<input class="dk-input" id="ns-email" name="email" type="email" autocomplete="email" required aria-describedby="ns-email-err ns-hint" data-err="ns-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="dk-btn" type="submit">Join</button>
</div>
<p class="dk-err" id="ns-email-err"></p>
<p class="dk-hint" id="ns-hint">One email around each new moon.</p>
</form>
<div class="dk-done" id="ns-done" tabindex="-1" hidden>
<div class="dk-done-row"><svg aria-hidden="true"><use href="#dk-i-tick"/></svg><div><b>Clear skies</b><p>Confirm <span data-email>your address</span> from the email we sent. The next edition goes out on 20 October.</p></div></div>
</div>
</div>
</section>
</div>
<div><span class="lbl">Gradient-edge card</span>
<section class="dk dk-edge" aria-labelledby="sn-title">
<div class="dk-ep"><i aria-hidden="true"><svg><use href="#dk-i-wave"/></svg></i><div><h3 id="sn-title">Signal & Noise</h3><small>ep. 94 · 38 min</small></div></div>
<p>Show notes, links and a transcript in your inbox the morning each episode drops.</p>
<form class="dk-form" action="#" method="post" data-nf="sn-done">
<label class="dk-label" for="sn-email">Email address</label>
<input class="dk-input" id="sn-email" name="email" type="email" autocomplete="email" required aria-describedby="sn-email-err sn-hint" data-err="sn-email-err"
data-missing="Enter your email address to get the show notes." data-bad="Enter an email address with an @ and a domain, like [email protected].">
<p class="dk-err" id="sn-email-err"></p>
<button class="dk-btn" type="submit">Get the show notes</button>
<p class="dk-hint" id="sn-hint">Every other Tuesday. Leave whenever you like.</p>
</form>
<div class="dk-done" id="sn-done" tabindex="-1" hidden>
<div class="dk-done-row"><svg aria-hidden="true"><use href="#dk-i-tick"/></svg><div><b>You're in</b><p>Episode 95's notes will reach <span data-email>you</span> once you confirm.</p></div></div>
</div>
</section>
</div>
</div>
CSS
/* Dark newsletter sign-ups. On a dark ground a light-theme form turns to mud, so every grey here is tuned:
body text and hints stay above 4.5:1, field borders above 3:1, and the accent is a light colour with dark text
on the button. Focus is a bright ring, errors use a light red that reads on the dark card. */
.dk{--dk-bg:#121826;--dk-field:#0b111d;--dk-ink:#e6eaf2;--dk-muted:#a3adc2;--dk-line:#66738f;--dk-edge:#263043;--dk-accent:#22d3ee;--dk-accent-ink:#062a33;
--dk-ring:rgba(34,211,238,.35);--dk-error:#fca5a5;--dk-ok:#86efac;--dk-mono:ui-monospace,"SF Mono",Menlo,Consolas,monospace;
background:var(--dk-bg);color:var(--dk-ink);border:1px solid var(--dk-edge);border-radius:20px;padding:30px;font-size:.92rem;line-height:1.5;
box-shadow:0 24px 60px -30px rgba(0,0,0,.8)}
.dk a{color:var(--dk-accent);font-weight:600;text-underline-offset:2px}
.dk a:focus-visible{outline:2px solid var(--dk-accent);outline-offset:2px;border-radius:3px}
.dk-label{display:block;font-size:.84rem;font-weight:650;margin-bottom:6px}
.dk-input{display:block;width:100%;min-width:0;font:inherit;color:var(--dk-ink);background:var(--dk-field);border:1.5px solid var(--dk-line);border-radius:10px;padding:10px 13px;
transition:border-color .15s,box-shadow .15s}
.dk-input::placeholder{color:#8994ab;opacity:1}
.dk-input:focus{outline:none;border-color:var(--dk-accent);box-shadow:0 0 0 4px var(--dk-ring)}
.dk-input.is-invalid{border-color:var(--dk-error)}
.dk-btn{flex:none;display:inline-flex;align-items:center;justify-content:center;gap:8px;font:inherit;font-weight:700;color:var(--dk-accent-ink);background:var(--dk-accent);border:0;border-radius:10px;
padding:10px 18px;cursor:pointer;transition:background-color .15s,box-shadow .15s}
.dk-btn:hover{background:#67e8f9;box-shadow:0 0 24px -4px rgba(34,211,238,.6)}
.dk-btn:focus-visible{outline:2px solid var(--dk-accent);outline-offset:3px}
.dk-btn svg{width:18px;height:18px}
.dk-err{display:flex;gap:7px;align-items:flex-start;font-size:.82rem;font-weight:600;color:var(--dk-error);margin-top:7px}
.dk-err:empty{display:none}
.dk-err::before{content:"!";flex:none;display:grid;place-items:center;width:16px;height:16px;margin-top:2px;border-radius:50%;background:var(--dk-error);color:#3b0a0a;font-size:.7rem;font-weight:800}
.dk-hint{font-size:.8rem;color:var(--dk-muted);margin-top:8px}
.dk-done{outline:none}
.dk-done[hidden],.dk-form[hidden]{display:none}
.dk-done:focus-visible{outline:2px solid var(--dk-accent);outline-offset:4px;border-radius:12px}
.dk-done-row{display:flex;gap:12px;align-items:flex-start;padding:14px 16px;border:1px solid rgba(134,239,172,.3);border-radius:12px;background:rgba(134,239,172,.08)}
.dk-done-row svg{flex:none;width:22px;height:22px;color:var(--dk-ok)}
.dk-done b{display:block}
.dk-done p{color:var(--dk-muted);font-size:.86rem}
.dk-done span[data-email]{color:var(--dk-ink);font-weight:600;overflow-wrap:anywhere}
/* 1. Developer newsletter bar */
.dk-dev{display:grid;grid-template-columns:1fr 1.1fr;gap:30px;align-items:center;padding:30px 32px;
background:radial-gradient(120% 140% at 100% 0%,rgba(34,211,238,.12),transparent 55%),var(--dk-bg)}
.dk-mono{font-family:var(--dk-mono);font-size:.78rem;color:var(--dk-accent)}
.dk-dev h3{font-size:1.5rem;font-weight:750;letter-spacing:-.02em;line-height:1.2;margin:6px 0}
.dk-dev p:not(.dk-mono){color:var(--dk-muted)}
.dk-join{display:flex;gap:6px;padding:5px;background:var(--dk-field);border:1.5px solid var(--dk-line);border-radius:13px;transition:border-color .15s,box-shadow .15s}
.dk-join:focus-within{border-color:var(--dk-accent);box-shadow:0 0 0 4px var(--dk-ring)}
.dk-join:has(.is-invalid){border-color:var(--dk-error)}
.dk-join .dk-input,.dk-join .dk-input:focus{border:0;box-shadow:none;background:transparent;padding:9px 10px;font-family:var(--dk-mono);font-size:.88rem}
.dk-join .dk-btn{border-radius:9px}
.dk-stat{display:flex;align-items:center;gap:8px;margin-top:12px;font-size:.8rem;color:var(--dk-muted)}
.dk-dot{width:8px;height:8px;border-radius:50%;background:var(--dk-ok);box-shadow:0 0 0 3px rgba(134,239,172,.18)}
/* 2. Card with a star-field header and topic chips */
.dk-sky{padding:0;overflow:hidden}
.dk-sky-head{position:relative;height:120px;padding:20px 24px;display:flex;align-items:flex-end;
background:radial-gradient(1.5px 1.5px at 12% 30%,#fff,transparent),radial-gradient(1px 1px at 28% 62%,#fff,transparent),radial-gradient(1.5px 1.5px at 46% 22%,#fff,transparent),
radial-gradient(1px 1px at 63% 48%,#fff,transparent),radial-gradient(2px 2px at 78% 26%,#fff,transparent),radial-gradient(1px 1px at 90% 66%,#fff,transparent),
radial-gradient(1px 1px at 8% 80%,#fff,transparent),radial-gradient(1.5px 1.5px at 56% 82%,#fff,transparent),
radial-gradient(ellipse at 70% 120%,rgba(167,139,250,.55),transparent 60%),linear-gradient(180deg,#0b1020,#1b1640)}
.dk-sky-head span{font-size:.72rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:#c4b5fd;background:rgba(10,14,22,.6);border-radius:999px;padding:3px 10px}
.dk-sky-body{padding:22px 24px 24px}
.dk-sky h3{font-size:1.35rem;font-weight:750;letter-spacing:-.02em}
.dk-sky-body>p{color:var(--dk-muted);margin:4px 0 16px}
.dk-set{border:0;min-width:0;margin-bottom:16px}
.dk-set legend{font-size:.84rem;font-weight:650;padding:0;margin-bottom:8px}
.dk-chips{display:flex;flex-wrap:wrap;gap:8px}
.dk-chip{position:relative;display:inline-flex;align-items:center;gap:6px;padding:6px 12px;border:1.5px solid var(--dk-line);border-radius:999px;font-size:.84rem;font-weight:600;color:var(--dk-ink);cursor:pointer;
transition:background-color .15s,border-color .15s,color .15s}
.dk-chip input{position:absolute;opacity:0;width:1px;height:1px;margin:0;pointer-events:none}
.dk-chip svg{width:14px;height:14px;display:none}
.dk-chip:hover{border-color:#c4b5fd}
.dk-chip:has(:checked){background:#c4b5fd;border-color:#c4b5fd;color:#1e1240}
.dk-chip:has(:checked) svg{display:block}
.dk-chip:has(:focus-visible){outline:2px solid #c4b5fd;outline-offset:2px}
.dk-row{display:flex;gap:8px}
.dk-row .dk-input{flex:1 1 0}
.dk-sky .dk-btn{background:#c4b5fd;color:#1e1240}
.dk-sky .dk-btn:hover{background:#ddd6fe;box-shadow:0 0 24px -4px rgba(196,181,253,.6)}
.dk-sky .dk-btn:focus-visible{outline-color:#c4b5fd}
.dk-sky .dk-input:focus{border-color:#c4b5fd;box-shadow:0 0 0 4px rgba(196,181,253,.3)}
/* 3. Gradient-edged compact card */
.dk-edge{position:relative;border:1px solid transparent;
background:linear-gradient(var(--dk-bg),var(--dk-bg)) padding-box,linear-gradient(135deg,#22d3ee,#a78bfa 50%,#f472b6) border-box}
.dk-edge h3{font-size:1.2rem;font-weight:750;letter-spacing:-.01em}
.dk-edge>p{color:var(--dk-muted);margin:4px 0 16px}
.dk-edge .dk-btn{width:100%;margin-top:10px}
.dk-ep{display:flex;align-items:center;gap:12px;margin-bottom:14px}
.dk-ep i{flex:none;display:grid;place-items:center;width:44px;height:44px;border-radius:12px;background:linear-gradient(135deg,#22d3ee,#a78bfa);color:#0a0e16;font-style:normal}
.dk-ep svg{width:24px;height:24px}
.dk-ep small{display:block;font-family:var(--dk-mono);font-size:.74rem;color:var(--dk-muted)}
@media (max-width:640px){.dk-dev{grid-template-columns:1fr;gap:20px;padding:26px 22px}}
@media (max-width:420px){.dk{padding:22px 18px}.dk-dev{padding:22px 18px}.dk-sky{padding:0}.dk-sky-body{padding:20px 18px 22px}.dk-row{flex-direction:column}.dk-join .dk-btn{padding:9px 12px}}
@media (prefers-reduced-motion:reduce){.dk-input,.dk-btn,.dk-join,.dk-chip{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 = el.checkValidity() ? '' : el.getAttribute(el.validity.valueMissing ? 'data-missing' : 'data-bad');
var err = document.getElementById(el.getAttribute('data-err'));
if (err) err.textContent = msg || '';
el.classList.toggle('is-invalid', !!msg);
if (msg) el.setAttribute('aria-invalid', 'true'); else el.removeAttribute('aria-invalid');
return !msg;
}
document.querySelectorAll('form[data-nf]').forEach(function (form) {
var done = document.getElementById(form.getAttribute('data-nf'));
var email = form.querySelector('input[type=email]');
form.noValidate = true;
form.addEventListener('submit', function (e) {
e.preventDefault();
var first = null;
form.querySelectorAll('[data-err]').forEach(function (el) { if (!check(el) && !first) first = el; });
if (first) { first.focus(); return; }
done.querySelectorAll('[data-email]').forEach(function (n) { n.textContent = email.value; });
form.hidden = true; done.hidden = false; done.focus();
});
form.addEventListener('input', function (e) { if (e.target.classList.contains('is-invalid')) check(e.target); });
});
})();
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="dk-i-tick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" 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="dk-i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.5l4.5 4.5L19 7.5"/></symbol>
<symbol id="dk-i-wave" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12h2M7 8v8M11 5v14M15 9v6M19 11v2M21 12h0"/></symbol>
</svg>
<div><span class="lbl">Developer newsletter bar</span>
<section class="card dk dk-dev" data-bs-theme="dark" aria-labelledby="st-title"><div class="card-body">
<div class="dk-pitch">
<p class="dk-mono">// issue #212 ships Friday</p>
<h3 id="st-title">Stack Trace Weekly</h3>
<p>Five links worth your time on databases, performance and the occasional war story. No hot takes.</p>
</div>
<div>
<form action="#" method="post" data-nf="st-done">
<label class="form-label" for="st-email">Email address</label>
<div class="input-group dk-join">
<input class="form-control" id="st-email" name="email" type="email" autocomplete="email" required placeholder="[email protected]" aria-describedby="st-email-err st-hint" data-err="st-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 dk-btn" type="submit">Subscribe</button>
</div>
<p class="dk-err" id="st-email-err"></p>
<p class="form-text" id="st-hint">Fridays only. Plain-text friendly. <a href="#privacy">Privacy</a></p>
</form>
<div class="dk-done" id="st-done" tabindex="-1" hidden>
<div class="dk-done-row"><svg aria-hidden="true"><use href="#dk-i-tick"/></svg><div><b>Check your inbox</b><p>Confirm <span data-email>your address</span> and issue #212 is yours on Friday.</p></div></div>
</div>
<p class="dk-stat"><span class="dk-dot" aria-hidden="true"></span>Read by 41,800 engineers</p>
</div>
</div></section>
</div>
<div class="demo-grid">
<div><span class="lbl">Star-field card with topics</span>
<section class="card dk dk-sky" data-bs-theme="dark" aria-labelledby="ns-title">
<div class="dk-sky-head"><span class="badge rounded-pill">New moon edition</span></div>
<div class="card-body">
<h3 id="ns-title">Nightshift</h3>
<p class="dk-sky-intro">What's worth looking up at this month, from a club of 300 amateur astronomers in the Peak District.</p>
<form action="#" method="post" data-nf="ns-done">
<fieldset class="dk-set">
<legend>Topics (optional)</legend>
<div class="d-flex flex-wrap gap-2">
<input class="btn-check" type="checkbox" name="topics" value="events" id="ns-t1" checked><label class="btn dk-chip" for="ns-t1"><svg aria-hidden="true"><use href="#dk-i-check"/></svg>Dark-sky events</label>
<input class="btn-check" type="checkbox" name="topics" value="gear" id="ns-t2"><label class="btn dk-chip" for="ns-t2"><svg aria-hidden="true"><use href="#dk-i-check"/></svg>Telescope reviews</label>
<input class="btn-check" type="checkbox" name="topics" value="photo" id="ns-t3" checked><label class="btn dk-chip" for="ns-t3"><svg aria-hidden="true"><use href="#dk-i-check"/></svg>Astrophotography</label>
<input class="btn-check" type="checkbox" name="topics" value="kids" id="ns-t4"><label class="btn dk-chip" for="ns-t4"><svg aria-hidden="true"><use href="#dk-i-check"/></svg>For kids</label>
</div>
</fieldset>
<label class="form-label" for="ns-email">Email address</label>
<div class="d-flex gap-2 dk-row">
<input class="form-control" id="ns-email" name="email" type="email" autocomplete="email" required aria-describedby="ns-email-err ns-hint" data-err="ns-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 dk-btn" type="submit">Join</button>
</div>
<p class="dk-err" id="ns-email-err"></p>
<p class="form-text" id="ns-hint">One email around each new moon.</p>
</form>
<div class="dk-done" id="ns-done" tabindex="-1" hidden>
<div class="dk-done-row"><svg aria-hidden="true"><use href="#dk-i-tick"/></svg><div><b>Clear skies</b><p>Confirm <span data-email>your address</span> from the email we sent. The next edition goes out on 20 October.</p></div></div>
</div>
</div>
</section>
</div>
<div><span class="lbl">Gradient-edge card</span>
<section class="card dk dk-edge" data-bs-theme="dark" aria-labelledby="sn-title"><div class="card-body">
<div class="dk-ep"><i aria-hidden="true"><svg><use href="#dk-i-wave"/></svg></i><div><h3 id="sn-title">Signal & Noise</h3><small>ep. 94 · 38 min</small></div></div>
<p class="dk-edge-intro">Show notes, links and a transcript in your inbox the morning each episode drops.</p>
<form action="#" method="post" data-nf="sn-done">
<label class="form-label" for="sn-email">Email address</label>
<input class="form-control" id="sn-email" name="email" type="email" autocomplete="email" required aria-describedby="sn-email-err sn-hint" data-err="sn-email-err"
data-missing="Enter your email address to get the show notes." data-bad="Enter an email address with an @ and a domain, like [email protected].">
<p class="dk-err" id="sn-email-err"></p>
<button class="btn dk-btn" type="submit">Get the show notes</button>
<p class="form-text" id="sn-hint">Every other Tuesday. Leave whenever you like.</p>
</form>
<div class="dk-done" id="sn-done" tabindex="-1" hidden>
<div class="dk-done-row"><svg aria-hidden="true"><use href="#dk-i-tick"/></svg><div><b>You're in</b><p>Episode 95's notes will reach <span data-email>you</span> once you confirm.</p></div></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 = el.checkValidity() ? '' : el.getAttribute(el.validity.valueMissing ? 'data-missing' : 'data-bad');
var err = document.getElementById(el.getAttribute('data-err'));
if (err) err.textContent = msg || '';
el.classList.toggle('is-invalid', !!msg);
if (msg) el.setAttribute('aria-invalid', 'true'); else el.removeAttribute('aria-invalid');
return !msg;
}
document.querySelectorAll('form[data-nf]').forEach(function (form) {
var done = document.getElementById(form.getAttribute('data-nf'));
var email = form.querySelector('input[type=email]');
form.noValidate = true;
form.addEventListener('submit', function (e) {
e.preventDefault();
var first = null;
form.querySelectorAll('[data-err]').forEach(function (el) { if (!check(el) && !first) first = el; });
if (first) { first.focus(); return; }
done.querySelectorAll('[data-email]').forEach(function (n) { n.textContent = email.value; });
form.hidden = true; done.hidden = false; done.focus();
});
form.addEventListener('input', function (e) { if (e.target.classList.contains('is-invalid')) check(e.target); });
});
})();
// 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 = el.checkValidity() ? '' : el.getAttribute(el.validity.valueMissing ? 'data-missing' : 'data-bad');
var err = document.getElementById(el.getAttribute('data-err'));
if (err) err.textContent = msg || '';
el.classList.toggle('is-invalid', !!msg);
if (msg) el.setAttribute('aria-invalid', 'true'); else el.removeAttribute('aria-invalid');
return !msg;
}
document.querySelectorAll('form[data-nf]').forEach(function (form) {
var done = document.getElementById(form.getAttribute('data-nf'));
var email = form.querySelector('input[type=email]');
form.noValidate = true;
form.addEventListener('submit', function (e) {
e.preventDefault();
var first = null;
form.querySelectorAll('[data-err]').forEach(function (el) { if (!check(el) && !first) first = el; });
if (first) { first.focus(); return; }
done.querySelectorAll('[data-email]').forEach(function (n) { n.textContent = email.value; });
form.hidden = true; done.hidden = false; done.focus();
});
form.addEventListener('input', function (e) { if (e.target.classList.contains('is-invalid')) check(e.target); });
});
})();
About this set
Three newsletter sign-ups for dark sites and apps, where a light-theme form loses its contrast. Developer newsletter bar is a two-column band with a monospace issue line, a joined field and button with a cyan glow on focus, a privacy link and a reader count. Star-field card has a night-sky header drawn with layered radial gradients, optional topic chips in lavender, an email row and a Join button. Gradient-edge card wraps a podcast show-notes sign-up in a cyan to pink border made with two background layers, no extra elements. Every grey is tuned against its background: body text and hints stay above 4.5:1, field borders above 3:1, buttons use a light accent with dark text, and errors use a light red that reads on the dark card. The Bootstrap version runs in Bootstrap’s own dark colour mode with data-bs-theme=”dark”. A short vanilla script validates the address, writes the error into the linked message and swaps in a green-tinted confirmation. Nothing is sent.
What’s included
Contrast-checked greys, borders and error colour on dark
Joined field with a focus glow
Star-field header and gradient border with CSS only
Optional topic chips from real checkboxes
Bootstrap version uses data-bs-theme="dark", .btn-check chips and .input-group
Accessibility
Each email field has a visible label, autocomplete="email", and its hint and error linked with aria-describedby; errors set aria-invalid and move focus to the field, and the confirmation is focused when it appears. Topic chips are checkboxes in a fieldset with a legend and show focus through :has(:focus-visible). Every control has a bright focus ring, and transitions switch off under prefers-reduced-motion.
Customise it
Change --dk-bg, --dk-field, --dk-line and --dk-muted on .dk for the surface, and --dk-accent with --dk-accent-ink for the button; the star-field card uses #c4b5fd for its accent. In Tailwind edit the dk-* colours and the dk-stars and dk-ring images in @theme.