Blocks · Forms
Newsletter signup
A heading, an email field with its button attached and a note on how often you send.
39% of the 2,062 sites in The UI Index have a form (806 sites).
One email a month
What shipped, what we learned, and one deploy tip. No tracking pixels.
Get the changelog in your inbox
One email a month
What shipped, what we learned, and one deploy tip. No tracking pixels.
Get the changelog in your inbox
Both previews render the same markup; the HTML tab below is that markup without a framework.
Install
- React
- npx shadcn@
latest add https:// www.themes.dev/ caseroom/ r/ newsletter.json - Astro
- npx shadcn@
latest add https:// www.themes.dev/ caseroom/ r/ astro/ newsletter.json
Or copy the files below into your project. They need clsx, tailwind-merge and the components they import, and the Caseroom theme. The HTML needs only the compiled CSS.
/** A newsletter sign-up (ported from the themes' sections/newsletter): a heading and one line, an email field with
* its button attached from 640px (stacked on phones), a note about how often you send. Native validation
* (type=email, required) with the field's own invalid style; the note is tied to the field. inline sits on the
* page; band is an inverted ground. */
export const newsletter = {
root: '@container bg-background px-6 py-20 text-foreground',
band: 'inverted',
inner: 'mx-auto grid max-w-6xl gap-10 @3xl:grid-cols-[1.4fr_1fr] @3xl:items-end',
title: 'text-2xl font-semibold tracking-[-0.03em] text-balance @md:text-3xl',
description: 'mt-3 max-w-xl text-base text-pretty text-muted-foreground',
form: 'flex w-full flex-col gap-2',
label: 'text-sm font-medium',
row: 'flex w-full flex-col gap-2 @md:flex-row @md:gap-0',
input: '@md:rounded-r-none @md:border-r-0',
button: '@md:rounded-l-none @md:h-9',
note: 'text-sm text-muted-foreground',
};
export type NewsletterContent = {
title: string;
description?: string;
/** Where the form posts. */
action: string;
button: string;
note?: string;
variant?: 'inline' | 'band';
/** Unique on the page; ids the field and its note. */
id: string;
};import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { cn } from '@/lib/utils';
import { newsletter as c, type NewsletterContent } from './newsletter.classes';
export type NewsletterProps = NewsletterContent & { className?: string };
/** A newsletter sign-up: heading, one line, an email field with its button, a note tied to the field. */
export function Newsletter({ title, description, action, button, note, variant = 'inline', id, className }: NewsletterProps) {
return (
<section aria-labelledby={`${id}-title`} className={cn(c.root, variant === 'band' && c.band, className)}>
<div className={c.inner}>
<div>
<h2 id={`${id}-title`} className={c.title}>{title}</h2>
{description && <p className={c.description}>{description}</p>}
</div>
<form action={action} method="post" className={c.form}>
<label htmlFor={`${id}-email`} className={c.label}>Email address</label>
<div className={c.row}>
<Input id={`${id}-email`} name="email" type="email" required autoComplete="email" placeholder="you@example.com" aria-describedby={note ? `${id}-note` : undefined} className={c.input} />
<Button type="submit" className={c.button}>{button}</Button>
</div>
{note && <p id={`${id}-note`} className={c.note}>{note}</p>}
</form>
</div>
</section>
);
}---
import Button from '@/components/ui/button.astro';
import Input from '@/components/ui/input.astro';
import { cn } from '@/lib/utils';
import { newsletter as c, type NewsletterContent } from './newsletter.classes';
type Props = NewsletterContent & { class?: string };
const { title, description, action, button, note, variant = 'inline', id, class: className } = Astro.props;
---
<section aria-labelledby={`${id}-title`} class={cn(c.root, variant === 'band' && c.band, className)}>
<div class={c.inner}>
<div>
<h2 id={`${id}-title`} class={c.title}>{title}</h2>
{description && <p class={c.description}>{description}</p>}
</div>
<form action={action} method="post" class={c.form}>
<label for={`${id}-email`} class={c.label}>Email address</label>
<div class={c.row}>
<Input id={`${id}-email`} name="email" type="email" required autocomplete="email" placeholder="you@example.com" aria-describedby={note ? `${id}-note` : undefined} class={c.input} />
<Button type="submit" class={c.button}>{button}</Button>
</div>
{note && <p id={`${id}-note`} class={c.note}>{note}</p>}
</form>
</div>
</section>/** A newsletter sign-up (ported from the themes' sections/newsletter): a heading and one line, an email field with
* its button attached from 640px (stacked on phones), a note about how often you send. Native validation
* (type=email, required) with the field's own invalid style; the note is tied to the field. inline sits on the
* page; band is an inverted ground. */
export const newsletter = {
root: '@container bg-background px-6 py-20 text-foreground',
band: 'inverted',
inner: 'mx-auto grid max-w-6xl gap-10 @3xl:grid-cols-[1.4fr_1fr] @3xl:items-end',
title: 'text-2xl font-semibold tracking-[-0.03em] text-balance @md:text-3xl',
description: 'mt-3 max-w-xl text-base text-pretty text-muted-foreground',
form: 'flex w-full flex-col gap-2',
label: 'text-sm font-medium',
row: 'flex w-full flex-col gap-2 @md:flex-row @md:gap-0',
input: '@md:rounded-r-none @md:border-r-0',
button: '@md:rounded-l-none @md:h-9',
note: 'text-sm text-muted-foreground',
};
export type NewsletterContent = {
title: string;
description?: string;
/** Where the form posts. */
action: string;
button: string;
note?: string;
variant?: 'inline' | 'band';
/** Unique on the page; ids the field and its note. */
id: string;
};<section aria-labelledby="ex-news-title" class="@container bg-background px-6 py-20 text-foreground">
<div class="mx-auto grid max-w-6xl gap-10 @3xl:grid-cols-[1.4fr_1fr] @3xl:items-end">
<div>
<h2 id="ex-news-title" class="text-2xl font-semibold tracking-[-0.03em] text-balance @md:text-3xl">One email a month</h2>
<p class="mt-3 max-w-xl text-base text-pretty text-muted-foreground">What shipped, what we learned, and one deploy tip. No tracking pixels.</p>
</div>
<form action="#" method="post" class="flex w-full flex-col gap-2">
<label for="ex-news-email" class="text-sm font-medium">Email address</label>
<div class="flex w-full flex-col gap-2 @md:flex-row @md:gap-0">
<input type="email" class="w-full min-w-0 rounded-md border border-input bg-background px-3 text-sm text-foreground transition-colors placeholder:text-muted-foreground enabled:hover:border-foreground/85 enabled:active:bg-muted aria-invalid:enabled:hover:border-destructive aria-invalid:enabled:hover:bg-[color-mix(in_oklab,var(--destructive)_6%,var(--background))] aria-invalid:enabled:active:bg-[color-mix(in_oklab,var(--destructive)_10%,var(--background))] focus-visible:border-ring focus-visible:outline-2 focus-visible:outline-offset-0 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:outline-destructive h-9 py-1 file:mr-3 file:border-0 file:bg-transparent file:text-sm file:font-medium @md:rounded-r-none @md:border-r-0" id="ex-news-email" name="email" required autocomplete="email" placeholder="you@example.com" aria-describedby="ex-news-note">
<button type="submit" class="inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,background-color,border-color,transform] select-none active:scale-[0.97] aria-busy:cursor-progress disabled:pointer-events-none disabled:opacity-50 aria-busy:disabled:opacity-80 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-[color-mix(in_oklab,var(--primary)_var(--hover-amount),var(--background))] active:bg-[color-mix(in_oklab,var(--primary)_var(--press-amount),var(--press-mix))] h-9 px-4 @md:rounded-l-none @md:h-9">
Subscribe
</button>
</div>
<p id="ex-news-note" class="text-sm text-muted-foreground">Unsubscribe with one click, any time.</p>
</form>
</div>
</section>
<section aria-labelledby="ex-news-band-title" class="@container bg-background px-6 py-20 text-foreground inverted">
<div class="mx-auto grid max-w-6xl gap-10 @3xl:grid-cols-[1.4fr_1fr] @3xl:items-end">
<div>
<h2 id="ex-news-band-title" class="text-2xl font-semibold tracking-[-0.03em] text-balance @md:text-3xl">Get the changelog in your inbox</h2>
</div>
<form action="#" method="post" class="flex w-full flex-col gap-2">
<label for="ex-news-band-email" class="text-sm font-medium">Email address</label>
<div class="flex w-full flex-col gap-2 @md:flex-row @md:gap-0">
<input type="email" class="w-full min-w-0 rounded-md border border-input bg-background px-3 text-sm text-foreground transition-colors placeholder:text-muted-foreground enabled:hover:border-foreground/85 enabled:active:bg-muted aria-invalid:enabled:hover:border-destructive aria-invalid:enabled:hover:bg-[color-mix(in_oklab,var(--destructive)_6%,var(--background))] aria-invalid:enabled:active:bg-[color-mix(in_oklab,var(--destructive)_10%,var(--background))] focus-visible:border-ring focus-visible:outline-2 focus-visible:outline-offset-0 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:outline-destructive h-9 py-1 file:mr-3 file:border-0 file:bg-transparent file:text-sm file:font-medium @md:rounded-r-none @md:border-r-0" id="ex-news-band-email" name="email" required autocomplete="email" placeholder="you@example.com" aria-describedby="ex-news-band-note">
<button type="submit" class="inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,background-color,border-color,transform] select-none active:scale-[0.97] aria-busy:cursor-progress disabled:pointer-events-none disabled:opacity-50 aria-busy:disabled:opacity-80 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-[color-mix(in_oklab,var(--primary)_var(--hover-amount),var(--background))] active:bg-[color-mix(in_oklab,var(--primary)_var(--press-amount),var(--press-mix))] h-9 px-4 @md:rounded-l-none @md:h-9">
Subscribe
</button>
</div>
<p id="ex-news-band-note" class="text-sm text-muted-foreground">About twice a month.</p>
</form>
</div>
</section>