ComponentsSite blocks

Contact form

Contact and quote requests feeding whatever lead store the app uses — CRM, email or a webhook. A server component around a ~2 KB useActionState island: the same validation runs in the browser and in your server action (validateContactForm, or the zod contactFormSchema), native HTML constraints cover the no-JS path, a honeypot and a time trap catch bots (answered with a silent fake success), and an error summary with role=alert links to each field. KVKK/GDPR: a linked privacy notice, consent boxes never pre-checked, marketing consent always separate.

React 19 useActionState blocks/site/contact-form.tsx, blocks/site/shared/contact-form-island.tsx, blocks/site/shared/site-form.ts, blocks/site/registry-2.tstheme · material · accent · density · font

Contact

Ask us anything

Questions about a treatment, a price or an appointment — we answer within one working day.

Address
Abdi İpekçi Caddesi 21, Kat 334367 Nişantaşı/Şişli
Hours
  • SunClosed
  • Mon–Thu9 AM–1 PM, 2 PM–7 PM
  • Fri9 AM–1 PM, 2 PM–5 PM
  • Sat10 AM–2 PM

We call back from 0216 000 00 00.

Installation

$ pnpm add @vamos-ui/react

The whole kit as one package — get updates by bumping the version. Add the styles to globals.css: @import "@vamos-ui/react/styles.css";

Usage

import { ContactForm, validateContactForm, type SiteFormState } from "@vamos-ui/react/blocks";

const fields = [{ name: "name", required: true }, { name: "email", required: true }, { name: "message", required: true }] as const;

async function sendLead(prev: SiteFormState, data: FormData): Promise<SiteFormState> {
  "use server";
  const v = validateContactForm(fields, data, { privacyCheckbox: true });
  if (v.spam) return { status: "success" };
  if (!v.ok) return v.state;
  await crm.createLead(v.values);
  return { status: "success" };
}

<ContactForm title="Ask us *anything*" fields={[...fields]} action={sendLead} privacyLink={{ href: "/kvkk" }} privacyCheckbox />

The block renderer has no way to pass a server action yet, so a stored contact-form block only validates until the app wires one (see the 06 plan: actions={{ contact }}).

Examples

Stacked quote request (agency)

Subject and budget as native selects (platform pickers on phones), date field, a custom submit label.

New project

Tell us about the project

A written quote within two working days. No sales calls unless you ask for one.

I have read the privacy notice.

Minimal (under a CTA)

Three fields and the button in one row.

Planning a group dinner?

Next.js server action

The action validates with the same config, drops spam silently and posts the lead. Without JavaScript the browser does a full round trip and gets the same states.

Write to the studio

States

Rendered from initialState: field errors with a linked summary (values kept), a server error with the fallback phone number, success.

Field errors

Please fill in full name.

Enter an email address like name@company.com.

Server error

Sent

Thanks Ayşe — we’ll call you back today before 18:00.

API

ContactForm

PropTypeDefault
variant
Layout.
"split" | "stacked" | "minimal""split"
fields
2–8 of name · email · phone · company · subject · budget · message · date, each { required?, label?, options?, placeholder?, hint?, max? }.
ContactFieldConfig[]name, email, message
action
Your server action, or a URL for a plain POST. Without it the form only validates (previews).
(prev: SiteFormState, data: FormData) => Promise<SiteFormState> | string—
successMessage
Replaces the form; focus moves to it.
ReactNode | RichDoc—
privacyLink / privacyCheckbox
Linked notice; with privacyCheckbox a required, never pre-checked box.
{ href; label? } / boolean—
marketingConsent
A separate, optional marketing box.
booleanfalse
info / showInfo
Contact column (split). phone is also the fallback on server errors.
{ address?; phone?; email?; hours? } / boolean—
submitLabel
Button text.
string"Send message"
initialState
Render a given state (docs, a server-rendered result).
SiteFormState—
eyebrow / title / lead
Section header.
ReactNode—
labels / locale
Field labels and error messages (site2LabelsTr for Turkish).
Partial<Site2Labels> / string—
theme / anchor / id / className / headingLevel / components / editMode
Shared block props.
BlockBaseProps—

validateContactForm(fields, data, opts)

PropTypeDefault
opts
spamMinMs: submits faster than this after render count as bots (default 2500).
{ privacyCheckbox?; labels?; locale?; spamMinMs? }—
returns
state is ready to return from the action.
{ ok; spam; values; errors; privacy; marketing; state }—

contactFormSchema(fields, opts) — blocks/site/registry-2

PropTypeDefault
returns
zod schema for Object.fromEntries(formData) (server only).
z.ZodObject—