Skip to content
6

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.json

Code

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&apos;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