Newsletter Form
Email subscription forms with single input, name + email, interest tags, GDPR consent, and success animation.
All variants use Alpine.js for state and submit simulation — no server
required for the demo. In production, wire @submit.prevent
to a fetch call or form action. The success state replaces the form
in-place so the surrounding layout never shifts.
1 — Single Field
The most compact form: one input, one button. Three layout configurations — stacked, inline, and inline pill — to fit any context from a hero banner to a footer strip.
Stacked
You're subscribed!
Check your inbox to confirm.
Inline
You're subscribed!
Pill / Rounded
You're subscribed!
<div x-data="{
email: '', loading: false, submitted: false, error: '',
submit() {
this.error = '';
if (!this.email || !this.email.includes('@')) {
this.error = 'Please enter a valid email address.'; return;
}
this.loading = true;
setTimeout(() => { this.loading = false; this.submitted = true; }, 1200);
}
}">
<div x-show="!submitted">
<div class="flex gap-2">
<input type="email" class="input input-bordered flex-1 rounded-[--radius-sm]"
placeholder="you@example.com" x-model="email"
@keydown.enter.prevent="submit()" />
<button type="button" class="btn btn-primary rounded-[--radius-sm]"
:class="{ 'loading loading-spinner': loading }"
@click="submit()">Subscribe</button>
</div>
<p class="text-xs text-error mt-1" x-show="error" x-text="error"></p>
</div>
<div x-show="submitted" class="flex items-center gap-2 py-3">
<span class="text-success">✓</span>
<p class="text-sm font-medium">You're subscribed!</p>
</div>
</div>
2 — Name + Email
Two-field form with individual field validation. First name is optional; email is required. Shows inline field-level errors on blur.
Stacked
No spam. Unsubscribe any time.
Check your inbox to confirm your subscription.
Card
Stay in the loop
Get weekly insights delivered to your inbox.
Confirm your email to complete signup.
<div x-data="{
name: '', email: '', loading: false, submitted: false,
errors: { email: '' },
validateEmail() {
this.errors.email = (!this.email || !this.email.includes('@'))
? 'Please enter a valid email address.' : '';
},
submit() {
this.validateEmail();
if (this.errors.email) return;
this.loading = true;
setTimeout(() => { this.loading = false; this.submitted = true; }, 1200);
}
}">
<div x-show="!submitted" class="flex flex-col gap-3">
<input type="text" class="input input-bordered rounded-[--radius-sm]"
placeholder="First name (optional)" x-model="name" />
<input type="email" class="input input-bordered rounded-[--radius-sm]"
:class="{ 'input-error': errors.email }"
placeholder="you@example.com"
x-model="email" @blur="validateEmail()" />
<p class="text-xs text-error" x-show="errors.email" x-text="errors.email"></p>
<button class="btn btn-primary rounded-[--radius-sm]"
:class="{ 'loading loading-spinner': loading }"
@click="submit()">Subscribe</button>
</div>
<div x-show="submitted" class="flex items-center gap-2 py-3">
<span class="text-success">✓</span>
<p x-text="name ? 'Thanks, ' + name + '!' : 'You\'re subscribed!'"></p>
</div>
</div>
3 — Interest Tags
Subscribers choose topics before submitting. Toggle-pill pattern built
with DaisyUI filter and Alpine — no custom CSS needed.
What are you interested in?
topics selected
You're subscribed!
We'll send you updates on .
<div x-data="{
email: '', loading: false, submitted: false, error: '',
interests: [
{ value: 'product', label: 'Product Updates', active: true },
{ value: 'tutorials', label: 'Tutorials', active: true },
{ value: 'events', label: 'Events', active: false },
// …
],
get selected() { return this.interests.filter(i => i.active).map(i => i.value); },
submit() {
this.error = '';
if (!this.email || !this.email.includes('@')) {
this.error = 'Please enter a valid email address.'; return;
}
this.loading = true;
setTimeout(() => { this.loading = false; this.submitted = true; }, 1200);
}
}">
<!-- Interest pills -->
<div class="flex flex-wrap gap-2">
<template x-for="tag in interests" :key="tag.value">
<button type="button"
class="badge badge-lg cursor-pointer select-none transition-colors"
:class="tag.active ? 'badge-primary' : 'badge-outline text-base-content/60'"
@click="tag.active = !tag.active"
:aria-pressed="tag.active">
<span x-text="tag.label"></span>
</button>
</template>
</div>
<!-- Email + submit -->
<input type="email" class="input input-bordered w-full rounded-[--radius-sm]"
placeholder="you@example.com" x-model="email" />
<p class="text-xs text-error" x-show="error" x-text="error"></p>
<button class="btn btn-primary rounded-[--radius-sm]"
:class="{ 'loading loading-spinner': loading }"
@click="submit()">Subscribe to selected topics</button>
</div>
4 — GDPR Consent
Submit button stays disabled until the consent checkbox is ticked. Consent text is a slot — drop in your actual privacy policy link.
Minimal
You're subscribed!
With frequency
Email frequency
You're subscribed!
Expect a digest in your inbox.
<div x-data="{
email: '', consent: false, loading: false, submitted: false,
errors: { email: '' },
validateEmail() {
this.errors.email = (!this.email || !this.email.includes('@'))
? 'Please enter a valid email address.' : '';
},
submit() {
this.validateEmail();
if (this.errors.email || !this.consent) return;
this.loading = true;
setTimeout(() => { this.loading = false; this.submitted = true; }, 1200);
}
}">
<input type="email" class="input input-bordered rounded-[--radius-sm] w-full"
placeholder="you@example.com"
x-model="email" @blur="validateEmail()"
:class="{ 'input-error': errors.email }" />
<p class="text-xs text-error mt-1" x-show="errors.email" x-text="errors.email"></p>
<label class="flex items-start gap-2.5 cursor-pointer">
<input type="checkbox" class="checkbox checkbox-sm checkbox-primary mt-0.5 shrink-0"
x-model="consent" />
<span class="text-xs text-base-content/70 leading-relaxed">
I agree to receive marketing emails and accept the
<a href="/privacy" class="link link-primary">Privacy Policy</a>.
</span>
</label>
<button class="btn btn-primary rounded-[--radius-sm] w-full"
:class="{ 'loading loading-spinner': loading }"
:disabled="!consent || loading"
@click="submit()">Subscribe</button>
</div>
5 — Dark Banner
Full-width dark strip commonly used as a pre-footer CTA section.
Uses DaisyUI's data-theme="dark" attribute so it stays
dark regardless of the page's light/dark mode.
Get the weekly digest
Curated insights on design, engineering, and product — every Thursday.
No spam. Unsubscribe any time.
You're subscribed!
First issue lands this Thursday.