Newsletter 1
Centered subscribe panel with an inline email form, inline validation and a success state that swaps in a confirmation once a valid address is entered.
Preview
Installation
npx shadcn@latest add https://hirael.com/r/newsletter-01.jsonCode
components/blocks/newsletter-01.tsx
'use client';
import * as React from 'react';
import { Check, Mail } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button } from '@/registry/hirael/bases/radix/ui/button';
import { Field, FieldError, FieldLabel } from '@/registry/hirael/bases/radix/ui/field';
import { Input } from '@/registry/hirael/bases/radix/ui/input';
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const Newsletter01 = () => {
const [email, setEmail] = React.useState('');
const [error, setError] = React.useState<string | null>(null);
const [subscribed, setSubscribed] = React.useState(false);
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!EMAIL_PATTERN.test(email.trim())) {
setError('Enter a valid email address.');
return;
}
setError(null);
setSubscribed(true);
}
return (
<section data-slot="newsletter" className="bg-background px-4 py-20 sm:py-28">
<div
data-slot="newsletter-panel"
className="relative mx-auto w-full max-w-2xl overflow-hidden rounded-xl border border-border bg-card p-8 text-center sm:p-12"
style={{
boxShadow: '0 24px 60px -34px color-mix(in oklch, var(--foreground) 24%, transparent)',
}}
>
<div
aria-hidden
className="pointer-events-none absolute inset-0"
style={{
backgroundImage:
'radial-gradient(120% 90% at 50% -10%, color-mix(in oklch, var(--primary) 12%, transparent), transparent 60%)',
}}
/>
<div className="relative z-10 flex flex-col items-center gap-5">
<span className="inline-flex size-11 items-center justify-center rounded-md border border-border bg-background">
<Mail className="size-5" />
</span>
{subscribed ? (
<div data-slot="newsletter-success" className="flex flex-col items-center gap-2">
<h2 className="flex items-center gap-2 font-serif text-3xl font-medium tracking-tight sm:text-4xl">
<Check className="size-6" />
You're on the list.
</h2>
<p className="text-sm text-muted-foreground sm:text-base">
Check your inbox to confirm the subscription.
</p>
</div>
) : (
<>
<div className="flex flex-col items-center gap-2">
<h2 className="font-serif text-3xl font-medium tracking-tight sm:text-4xl">
New components, in your inbox.
</h2>
<p className="max-w-md text-sm text-muted-foreground sm:text-base">
A short note when we ship something. New components, blocks, and the occasional deep dive.
</p>
</div>
<form onSubmit={handleSubmit} noValidate className="flex w-full max-w-md flex-col gap-3 sm:flex-row">
<Field className="gap-1.5 text-start" data-invalid={error ? true : undefined}>
<FieldLabel htmlFor="newsletter-email" className="sr-only">
Email address
</FieldLabel>
<Input
id="newsletter-email"
type="email"
inputMode="email"
autoComplete="email"
placeholder="you@company.com"
value={email}
onChange={(e) => {
setEmail(e.target.value);
if (error) setError(null);
}}
aria-invalid={error ? true : undefined}
aria-describedby={error ? 'newsletter-error' : undefined}
className={cn(error && 'border-destructive')}
/>
<FieldError id="newsletter-error" className="text-xs">
{error}
</FieldError>
</Field>
<Button type="submit" className="shrink-0">
Subscribe
</Button>
</form>
<div className="flex items-center gap-3">
<div className="flex -space-x-2 rtl:space-x-reverse">
{['MS', 'AK', 'JD', 'RL'].map((initials) => (
<span
key={initials}
className="inline-flex size-7 items-center justify-center rounded-full border-2 border-card bg-muted font-mono text-[10px] font-medium text-muted-foreground"
>
{initials}
</span>
))}
</div>
<span className="text-xs text-muted-foreground">Join 1,200+ developers on the list.</span>
</div>
<p className="font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground">
Only release notes. Unsubscribe in one click.
</p>
</>
)}
</div>
</div>
</section>
);
};
export default Newsletter01;
Dependencies
shadcn registry
buttonfieldinput
npm
lucide-react