# Contact form (Caseroom block)

Name, email, topic and message with visible labels and native validation. Black and white, dark mode, shadcn-compatible; React, Astro and plain HTML.

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

## 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`
- HTML: https://www.themes.dev/caseroom/r/html/contact-form.html (with https://www.themes.dev/caseroom/r/html/caseroom.css)

Page: https://www.themes.dev/caseroom/blocks/contact-form/ · Rules for agents: https://www.themes.dev/caseroom/AGENT.md

## Code

### contact-form.astro

```astro
---
import Button from '@/components/ui/button.astro';
import Input from '@/components/ui/input.astro';
import Label from '@/components/ui/label.astro';
import Select from '@/components/ui/select.astro';
import Textarea from '@/components/ui/textarea.astro';
import { cn } from '@/lib/utils';
import { contactForm as c, type ContactContent } from './contact-form.classes';

type Props = ContactContent & { class?: string };
const { title, description, action, topics, button, id, class: className } = Astro.props;
---

<section aria-labelledby={`${id}-title`} class={cn(c.root, 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}>
      <div class={c.field}>
        <Label for={`${id}-name`}>Name</Label>
        <Input id={`${id}-name`} name="name" autocomplete="name" required />
      </div>
      <div class={c.field}>
        <Label for={`${id}-email`}>Email</Label>
        <Input id={`${id}-email`} name="email" type="email" autocomplete="email" required />
      </div>
      <div class={cn(c.field, c.wide)}>
        <Label for={`${id}-topic`}>Topic</Label>
        <Select id={`${id}-topic`} name="topic" placeholder="Choose a topic" options={topics} />
      </div>
      <div class={cn(c.field, c.wide)}>
        <Label for={`${id}-message`}>Message</Label>
        <Textarea id={`${id}-message`} name="message" rows={5} required aria-describedby={`${id}-message-hint`} />
        <p id={`${id}-message-hint`} class={c.hint}>A few sentences is plenty. We reply within one working day.</p>
      </div>
      <div class={c.footer}>
        <Button type="submit">{button}</Button>
        <p class={c.hint}>All fields except the topic are required.</p>
      </div>
    </form>
  </div>
</section>
```

### contact-form.classes.ts

```tsx
/** 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

```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>
  );
}
```
