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.
Talk to us
Questions about plans, a deploy that won't go through, or anything else.
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.
/** 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;
};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>
);
}---
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>/** 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;
};<section aria-labelledby="ex-contact-title" class="@container bg-background px-6 py-20 text-foreground">
<div class="mx-auto grid max-w-6xl gap-10 @3xl:grid-cols-[1fr_1.4fr]">
<div>
<h2 id="ex-contact-title" class="text-3xl font-semibold tracking-[-0.03em] text-balance">Talk to us</h2>
<p class="mt-3 max-w-md text-base text-pretty text-muted-foreground">Questions about plans, a deploy that won't go through, or anything else.</p>
</div>
<form action="#" method="post" class="grid gap-5 @lg:grid-cols-2">
<div class="grid gap-2">
<label class="flex items-center gap-2 text-sm leading-none font-medium has-[input]:items-start has-[input]:leading-snug has-[input]:[&>*:first-child]:mt-px select-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50" for="ex-contact-name">
Name
</label>
<input type="text" 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" id="ex-contact-name" name="name" autocomplete="name" required>
</div>
<div class="grid gap-2">
<label class="flex items-center gap-2 text-sm leading-none font-medium has-[input]:items-start has-[input]:leading-snug has-[input]:[&>*:first-child]:mt-px select-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50" for="ex-contact-email">
Email
</label>
<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" id="ex-contact-email" name="email" autocomplete="email" required>
</div>
<div class="grid gap-2 @lg:col-span-2">
<label class="flex items-center gap-2 text-sm leading-none font-medium has-[input]:items-start has-[input]:leading-snug has-[input]:[&>*:first-child]:mt-px select-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50" for="ex-contact-topic">
Topic
</label>
<div class="relative w-full">
<select 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 cursor-pointer appearance-none py-1 pr-9 has-[option[value='']:checked]:text-muted-foreground [&_option]:text-foreground" id="ex-contact-topic" name="topic">
<option value="" disabled selected>Choose a topic</option>
<option value="sales">Pricing and plans</option>
<option value="support">Help with a deploy</option>
<option value="press">Press</option>
</select>
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="pointer-events-none absolute top-1/2 right-3 size-4 -translate-y-1/2 text-muted-foreground"><path d="M4 6l4 4 4-4"></path></svg>
</div>
</div>
<div class="grid gap-2 @lg:col-span-2">
<label class="flex items-center gap-2 text-sm leading-none font-medium has-[input]:items-start has-[input]:leading-snug has-[input]:[&>*:first-child]:mt-px select-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50" for="ex-contact-message">
Message
</label>
<textarea 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 min-h-20 py-2 [field-sizing:content]" id="ex-contact-message" name="message" rows="5" required aria-describedby="ex-contact-message-hint">
</textarea>
<p id="ex-contact-message-hint" class="text-sm text-muted-foreground">A few sentences is plenty. We reply within one working day.</p>
</div>
<div class="flex flex-wrap items-center gap-4 @lg:col-span-2">
<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">
Send message
</button>
<p class="text-sm text-muted-foreground">All fields except the topic are required.</p>
</div>
</form>
</div>
</section>