Contact Form with Topic Select and Details Panel

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.

Code

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

When to use it

How to customize it

Accessibility

Questions

How do I make this contact form send an email?

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.

How do I add a phone number field?

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.

Why use a select for the topic instead of free text?

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.

Can I remove the details panel?

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.

Does it work with Tailwind CSS v3 and v4?

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.

More in Forms

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.

Product

Who It's For

Features

Resources

Company

View Sitemap