Skip to content
Caseroom

Blocks · Forms

Contact form

Name, email, topic and message with visible labels and native validation.

39% of the 2,062 sites in The UI Index have a form (806 sites).

Talk to us

Questions about plans, a deploy that won't go through, or anything else.

A few sentences is plenty. We reply within one working day.

All fields except the topic are required.

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/contact-form.json
Astro
npx shadcn@latest add https://www.themes.dev/caseroom/r/astro/contact-form.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.

contact-form.classes.ts
/** A contact form: a heading and a line on the left, the form on the right from a wide container. Visible labels,
 *  native validation, the required fields marked in words, one primary button. */
export const contactForm = {
  root: '@container bg-background px-6 py-20 text-foreground',
  inner: 'mx-auto grid max-w-6xl gap-10 @3xl:grid-cols-[1fr_1.4fr]',
  title: 'text-3xl font-semibold tracking-[-0.03em] text-balance',
  description: 'mt-3 max-w-md text-base text-pretty text-muted-foreground',
  form: 'grid gap-5 @lg:grid-cols-2',
  field: 'grid gap-2',
  wide: '@lg:col-span-2',
  hint: 'text-sm text-muted-foreground',
  footer: 'flex flex-wrap items-center gap-4 @lg:col-span-2',
};

export type ContactContent = {
  title: string;
  description?: string;
  action: string;
  topics: { value: string; label: string }[];
  button: string;
  /** Unique on the page; prefixes the field ids. */
  id: string;
};
contact-form.tsx
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select } from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea';
import { cn } from '@/lib/utils';
import { contactForm as c, type ContactContent } from './contact-form.classes';

export type ContactFormProps = ContactContent & { className?: string };

/** A contact form: name, email, topic and message with visible labels and native validation. */
export function ContactForm({ title, description, action, topics, button, id, className }: ContactFormProps) {
  return (
    <section aria-labelledby={`${id}-title`} className={cn(c.root, 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}>
          <div className={c.field}>
            <Label htmlFor={`${id}-name`}>Name</Label>
            <Input id={`${id}-name`} name="name" autoComplete="name" required />
          </div>
          <div className={c.field}>
            <Label htmlFor={`${id}-email`}>Email</Label>
            <Input id={`${id}-email`} name="email" type="email" autoComplete="email" required />
          </div>
          <div className={cn(c.field, c.wide)}>
            <Label htmlFor={`${id}-topic`}>Topic</Label>
            <Select id={`${id}-topic`} name="topic" placeholder="Choose a topic" options={topics} />
          </div>
          <div className={cn(c.field, c.wide)}>
            <Label htmlFor={`${id}-message`}>Message</Label>
            <Textarea id={`${id}-message`} name="message" rows={5} required aria-describedby={`${id}-message-hint`} />
            <p id={`${id}-message-hint`} className={c.hint}>A few sentences is plenty. We reply within one working day.</p>
          </div>
          <div className={c.footer}>
            <Button type="submit">{button}</Button>
            <p className={c.hint}>All fields except the topic are required.</p>
          </div>
        </form>
      </div>
    </section>
  );
}