# Contact Form With Floating Labels in HTML and CSS

> Free contact form in HTML and CSS with floating labels, :user-invalid error styling, messages tied by aria-describedby, and a React and Tailwind version.

A shadcn/ui block from the [@weinc](https://we.inc/components?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/weinc/contact-form-with-floating-labels-html-css).

- Page, with a live preview: https://shadcn.exchange/r/weinc/contact-form-with-floating-labels-html-css
- Screenshot: https://shadcn.exchange/shot/weinc/contact-form-with-floating-labels-html-css/light.png
- Registry item: https://we.inc/r/contact-form-with-floating-labels-html-css.json
- Listed: 2026-10-29, source last changed 2026-10-29

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/weinc/contact-form-with-floating-labels-html-css.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Files

### `components/we-inc/contact-form-with-floating-labels-html-css.tsx`

```tsx
import { useState, type FocusEvent, type FormEvent } from "react";

type Field = HTMLInputElement | HTMLTextAreaElement;

// Field list: change labels, types and error text here.
const FIELDS = [
  { name: "name", label: "Your name", type: "text", auto: "name", error: "Please tell us your name." },
  { name: "email", label: "Email address", type: "email", auto: "email", error: "Enter an email like ana@example.com." },
] as const;
const MESSAGE_ERROR = "A sentence or two helps us quote, at least 10 characters.";

export default function ContactFormWithFloatingLabels() {
  // A field only shows its error after it has been left once, like CSS :user-invalid.
  const [bad, setBad] = useState<Record<string, boolean>>({});
  const [sent, setSent] = useState(false);

  const check = (e: FocusEvent<Field>) => setBad((b) => ({ ...b, [e.target.name]: !e.target.validity.valid }));
  const recheck = (e: FormEvent<Field>) => {
    const el = e.currentTarget;
    if (bad[el.name] && el.validity.valid) setBad((b) => ({ ...b, [el.name]: false }));
  };
  const submit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const form = e.currentTarget;
    const next: Record<string, boolean> = {};
    for (const el of Array.from(form.elements) as Field[]) if (el.name) next[el.name] = !el.validity.valid;
    setBad(next);
    const first = form.querySelector<HTMLElement>(":invalid");
    if (first) first.focus();
    else setSent(true);
  };

  // The peer classes float the label when the field has focus or text.
  const box = (n: string) =>
    `peer block w-full rounded-xl border bg-white px-4 pb-2 pt-6 text-[15px] text-slate-900 placeholder-transparent focus:outline-none focus:ring-4 ${bad[n] ? "border-rose-500 focus:ring-rose-100" : "border-slate-300 focus:border-teal-700 focus:ring-teal-100"}`;
  const label = "pointer-events-none absolute left-4 top-2 text-xs font-medium text-slate-600 transition-all duration-150 peer-placeholder-shown:top-4 peer-placeholder-shown:text-[15px] peer-focus:top-2 peer-focus:text-xs peer-focus:text-teal-800 motion-reduce:transition-none";
  const err = (n: string, text: string) => bad[n] ? <p id={`cffl-${n}-err`} className="mt-1.5 text-sm text-rose-700">{text}</p> : null;

  return (
    <section className="bg-[#eef3f1] px-4 py-16 sm:py-20">
      <div className="mx-auto grid max-w-4xl gap-10 md:grid-cols-[1fr_1.3fr]">
        <div>
          <p className="text-xs font-semibold uppercase tracking-[0.2em] text-teal-800">Saltmarsh Joinery</p>
          <h2 className="mt-3 text-3xl font-semibold tracking-tight text-slate-900">Tell us about your project</h2>
          <p className="mt-3 text-slate-700">Kitchens, built-in shelving and staircases across the county. We reply within two working days.</p>
          <dl className="mt-6 space-y-2 text-sm text-slate-700">
            <div><dt className="inline font-medium text-slate-900">Workshop: </dt><dd className="inline">Unit 4, Quay Lane</dd></div>
            <div><dt className="inline font-medium text-slate-900">Phone: </dt><dd className="inline">01632 960 418</dd></div>
          </dl>
        </div>

        <form noValidate onSubmit={submit} className="rounded-2xl bg-white p-6 shadow-sm ring-1 ring-slate-200 sm:p-8">
          {sent ? (
            <div role="status" className="py-10 text-center">
              <p className="text-lg font-semibold text-slate-900">Thanks, your message is in.</p>
              <p className="mt-2 text-slate-700">Mara will get back to you by Thursday.</p>
            </div>
          ) : (
            <div className="space-y-5">
              {FIELDS.map((f) => (
                <div key={f.name}>
                  <div className="relative">
                    <input
                      id={`cffl-${f.name}`}
                      name={f.name}
                      type={f.type}
                      autoComplete={f.auto}
                      required
                      placeholder={f.label}
                      onBlur={check}
                      onInput={recheck}
                      aria-invalid={bad[f.name] || undefined}
                      aria-describedby={bad[f.name] ? `cffl-${f.name}-err` : undefined}
                      className={box(f.name)}
                    />
                    <label htmlFor={`cffl-${f.name}`} className={label}>{f.label}</label>
                  </div>
                  {err(f.name, f.error)}
                </div>
              ))}
              <div>
                <div className="relative">
                  <textarea
                    id="cffl-message"
                    name="message"
                    rows={4}
                    required
                    minLength={10}
                    placeholder="What would you like built?"
                    onBlur={check}
                    onInput={recheck}
                    aria-invalid={bad.message || undefined}
                    aria-describedby={bad.message ? "cffl-message-err" : undefined}
                    className={`${box("message")} resize-y`}
                  />
                  <label htmlFor="cffl-message" className={label}>What would you like built?</label>
                </div>
                {err("message", MESSAGE_ERROR)}
              </div>
              <button type="submit" className="h-12 w-full rounded-xl bg-teal-800 text-[15px] font-semibold text-white hover:bg-teal-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-teal-800 focus-visible:ring-offset-2">Send message</button>
            </div>
          )}
        </form>
      </div>
    </section>
  );
}
```
