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.
Contact
Ask us anything
Questions about a treatment, a price or an appointment — we answer within one working day.
- Phone
- +90 216 000 00 00
- 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
Installation
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.
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
Server error
Sent
API
ContactForm
| Prop | Type | Default |
|---|---|---|
variantLayout. | "split" | "stacked" | "minimal" | "split" |
fields2–8 of name · email · phone · company · subject · budget · message · date, each { required?, label?, options?, placeholder?, hint?, max? }. | ContactFieldConfig[] | name, email, message |
actionYour server action, or a URL for a plain POST. Without it the form only validates (previews). | (prev: SiteFormState, data: FormData) => Promise<SiteFormState> | string | — |
successMessageReplaces the form; focus moves to it. | ReactNode | RichDoc | — |
privacyLink / privacyCheckboxLinked notice; with privacyCheckbox a required, never pre-checked box. | { href; label? } / boolean | — |
marketingConsentA separate, optional marketing box. | boolean | false |
info / showInfoContact column (split). phone is also the fallback on server errors. | { address?; phone?; email?; hours? } / boolean | — |
submitLabelButton text. | string | "Send message" |
initialStateRender a given state (docs, a server-rendered result). | SiteFormState | — |
eyebrow / title / leadSection header. | ReactNode | — |
labels / localeField labels and error messages (site2LabelsTr for Turkish). | Partial<Site2Labels> / string | — |
theme / anchor / id / className / headingLevel / components / editModeShared block props. | BlockBaseProps | — |
validateContactForm(fields, data, opts)
| Prop | Type | Default |
|---|---|---|
optsspamMinMs: submits faster than this after render count as bots (default 2500). | { privacyCheckbox?; labels?; locale?; spamMinMs? } | — |
returnsstate is ready to return from the action. | { ok; spam; values; errors; privacy; marketing; state } | — |
contactFormSchema(fields, opts) — blocks/site/registry-2
| Prop | Type | Default |
|---|---|---|
returnszod schema for Object.fromEntries(formData) (server only). | z.ZodObject | — |