A contact section in two parts. A dark teal panel on the left holds a heading, one sentence about reply times and three contact details with icons. The form on the right has name and email side by side, a topic select, a message box with a live character counter and a send button. On phones the panel sits on top and every field takes the full width.
import { useState } from "react";
const topics = ["A quote for a new project", "An order I already placed", "Opening a trade account", "Something else"];
const details = [
{ label: "Email", value: "[email protected]", icon: "M3 7.5 12 13l9-5.5M4.5 5h15A1.5 1.5 0 0 1 21 6.5v11a1.5 1.5 0 0 1-1.5 1.5h-15A1.5 1.5 0 0 1 3 17.5v-11A1.5 1.5 0 0 1 4.5 5Z" },
{ label: "Phone", value: "(555) 014-2290", icon: "M5 4h3.5l1.5 4-2 1.5a11 11 0 0 0 6.5 6.5l1.5-2 4 1.5V19a1.5 1.5 0 0 1-1.5 1.5C10.5 20.5 3.5 13.5 3.5 5.5A1.5 1.5 0 0 1 5 4Z" },
{ label: "Workshop", value: "14 Quay Lane, open Monday to Friday, 8am to 5pm", icon: "M12 21s7-6.2 7-11.5a7 7 0 1 0-14 0C5 14.8 12 21 12 21Zm0-9a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Z" },
];
const MAX_LENGTH = 600;
const field = "mt-2 block h-11 w-full rounded-xl border border-stone-300 bg-white px-3.5 text-sm text-stone-900 placeholder:text-stone-400 focus:border-teal-700 focus:outline-none focus:ring-2 focus:ring-teal-700/20";
export default function ContactFormWithTopicSelect() {
const [message, setMessage] = useState("");
const [sent, setSent] = useState(false);
return (
<section className="bg-stone-100 px-4 py-12 text-stone-900 sm:px-6 sm:py-20">
<div className="mx-auto grid max-w-5xl overflow-hidden rounded-3xl bg-white ring-1 ring-stone-200 lg:grid-cols-5">
<div className="flex flex-col bg-teal-900 p-8 text-white sm:p-10 lg:col-span-2">
<p className="text-xs font-semibold uppercase tracking-widest text-teal-200">Contact</p>
<h2 className="mt-4 text-3xl font-semibold tracking-tight">Tell us what you are building</h2>
<p className="mt-4 text-sm leading-relaxed text-teal-100">
Send a few lines about the room, the timber you like and your timing. A joiner reads every message and replies within one working day.
</p>
<ul className="mt-10 space-y-6 lg:mt-auto lg:pt-12">
{details.map((d) => (
<li key={d.label} className="flex items-start gap-4">
<span className="flex h-10 w-10 flex-none items-center justify-center rounded-xl bg-teal-800">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="h-5 w-5 text-teal-100">
<path d={d.icon} />
</svg>
</span>
<div className="min-w-0">
<p className="text-xs font-medium text-teal-200">{d.label}</p>
<p className="mt-0.5 break-words text-sm font-medium">{d.value}</p>
</div>
</li>
))}
</ul>
</div>
<form
action="#"
onSubmit={(e) => {
e.preventDefault();
setSent(true);
}}
className="p-8 sm:p-10 lg:col-span-3"
>
<div className="grid gap-5 sm:grid-cols-2">
<div>
<label htmlFor="contact-name" className="text-sm font-medium">Full name</label>
<input id="contact-name" name="name" type="text" required autoComplete="name" placeholder="Priya Raman" className={field} />
</div>
<div>
<label htmlFor="contact-email" className="text-sm font-medium">Email</label>
<input id="contact-email" name="email" type="email" required autoComplete="email" placeholder="[email protected]" className={field} />
</div>
<div className="sm:col-span-2">
<label htmlFor="contact-topic" className="text-sm font-medium">What is this about?</label>
<div className="relative">
<select id="contact-topic" name="topic" defaultValue={topics[0]} className={`${field} appearance-none pr-10`}>
{topics.map((t) => (
<option key={t}>{t}</option>
))}
</select>
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="pointer-events-none absolute right-3.5 top-1/2 mt-1 h-4 w-4 -translate-y-1/2 text-stone-600">
<path d="m5 7.5 5 5 5-5" />
</svg>
</div>
</div>
<div className="sm:col-span-2">
<div className="flex items-baseline justify-between">
<label htmlFor="contact-message" className="text-sm font-medium">Message</label>
<span className="text-xs tabular-nums text-stone-600" aria-live="polite">{message.length} / {MAX_LENGTH}</span>
</div>
<textarea
id="contact-message"
name="message"
required
rows={5}
maxLength={MAX_LENGTH}
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="Rough sizes, the finish you have in mind and when you need it by."
className={`${field} h-auto resize-none py-3 leading-relaxed`}
/>
</div>
</div>
<div className="mt-7 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<p className="max-w-xs text-xs leading-relaxed text-stone-600" role="status">
{sent ? "Thanks. Your message is on its way and we will reply by email." : "We only use your details to answer this message."}
</p>
<button type="submit" className="inline-flex h-11 flex-none items-center justify-center gap-2 rounded-xl bg-teal-800 px-6 text-sm font-semibold text-white transition hover:bg-teal-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-teal-700 focus-visible:ring-offset-2">
Send message
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="h-4 w-4">
<path d="M4 10h12m-4.5-4.500L16 10l-4.5 4.5" />
</svg>
</button>
</div>
</form>
</div>
</section>
);
}
Install with one command: npx shadcn@latest add https://we.inc/r/contact-form-with-topic-select.json
The form does not send anything on its own. Point `action` at a form handling service or your own server route, or keep the `onSubmit` handler and post the fields with `fetch`. Show the confirmation line only after the request succeeds.
Copy the email block, change the `id`, `name` and label, and set `type="tel"` with `autoComplete="tel"`. Inside the `sm:grid-cols-2` grid it will sit beside another field on wider screens.
A fixed list lets you route the message to the right inbox and gives you clean data to count later. Keep the list short and end it with a catch-all option.
Yes. Delete the first child of the grid, remove `lg:grid-cols-5` from the grid and `lg:col-span-3` from the form, then lower `max-w-5xl` to `max-w-2xl` so the form does not stretch.
Yes. It uses core utility classes only, with no forms plugin and no custom theme values.
Related: Dark Sign-In Form with Show Password Toggle, Multi-Step Signup Form with Step Indicator, Split Hero with Email Capture, Button Set: Primary, Secondary, Ghost, Icon and Loading.
More forms or all free UI components. Start for free to have a full site built around it.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.