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.