Simple Footer
A logo, one row of links and a copyright line - stacked on mobile, spread on desktop.
6 frameworksBeginner
An email capture beside two link columns, with inline validation announced to screen readers.
<!--
An email capture is a form, and the parts people skip are the ones that make
it usable:
- A real <label>. "Email address" as placeholder text vanishes the moment you
type, so the field loses its name exactly when you need to check it. The
label is visually hidden here, not absent.
- type="email" + autocomplete="email" - the phone keyboard and the browser's
autofill both hang off these.
- The result goes in an aria-live region. Turning the button green tells a
sighted user it worked and tells everyone else nothing.
-->
<footer class="footnews">
<div class="footnews__inner">
<div class="footnews__pitch">
<h2 class="footnews__title">Ship better, weekly</h2>
<p class="footnews__blurb">Product news and engineering notes. One email a week, no filler.</p>
<form class="footnews__form" novalidate>
<label class="footnews__label" for="footnews-email">Email address</label>
<div class="footnews__row">
<input
class="footnews__input"
id="footnews-email"
name="email"
type="email"
autocomplete="email"
placeholder="you@company.com"
required
/>
<button class="footnews__submit" type="submit">Subscribe</button>
</div>
<p class="footnews__status" role="status" aria-live="polite"></p>
</form>
</div>
<nav class="footnews__nav" aria-label="Footer">
<div class="footnews__cols">
<div>
<h2 class="footnews__heading">Product</h2>
<ul class="footnews__list">
<li><a class="footnews__link" href="/features">Features</a></li>
<li><a class="footnews__link" href="/pricing">Pricing</a></li>
<li><a class="footnews__link" href="/changelog">Changelog</a></li>
</ul>
</div>
<div>
<h2 class="footnews__heading">Company</h2>
<ul class="footnews__list">
<li><a class="footnews__link" href="/about">About</a></li>
<li><a class="footnews__link" href="/blog">Blog</a></li>
<li><a class="footnews__link" href="/careers">Careers</a></li>
</ul>
</div>
</div>
</nav>
</div>
<div class="footnews__bar">
<p class="footnews__copy">© <time datetime="2026">2026</time> Adysre Inc.</p>
</div>
</footer>
<script>
document.querySelectorAll('.footnews__form').forEach(function (form) {
var input = form.querySelector('.footnews__input');
var status = form.querySelector('.footnews__status');
form.addEventListener('submit', function (event) {
event.preventDefault();
// novalidate turns off the browser bubble so the message lands in the
// live region instead - one channel, announced, styled like the page.
if (!input.checkValidity()) {
status.textContent = 'Enter a valid email address.';
status.dataset.state = 'error';
input.setAttribute('aria-invalid', 'true');
input.focus();
return;
}
input.removeAttribute('aria-invalid');
status.textContent = 'Thanks - check your inbox to confirm.';
status.dataset.state = 'ok';
form.reset();
});
});
</script>| Prop | Type | Default | Description |
|---|---|---|---|
ctaLabel | string | 'Subscribe' | Call-to-action button text. |
copy | string | - | Body text shown under the title. |
className | string | - | Additional classes merged onto the root element. |
The parts people skip are the ones that make an email capture usable. A real `<label>`, visually hidden but present - placeholder text vanishes the instant you type, so the field loses its name exactly when you want to check what you typed. `type="email"` and `autocomplete="email"`, which is what summons the @-key phone keyboard and the browser's autofill. And the result in an `aria-live` region: turning the button green tells a sighted user it worked and tells everyone else nothing. `noValidate` suppresses the browser's own bubble so there is one message channel, not two, while `checkValidity()` still gives you the parsing for free. The status colours are the fiddly part - `red-600`/`green-600` clear AA on white but fail on `gray-900`, so dark mode lifts both to the 400 shades. Swap `onSubmit` for a Server Action or your provider's endpoint.