section-form

star 2

Adds forms to Astro pages using React with Zod validation, submitting to a Cloudflare Workers handler. Use when user requests "add form", "contact form", "newsletter signup", "inquiry form", "booking form", "feedback form", "registration form", "waitlist", or any form functionality requiring client-side validation and server submission. Skip for pure mailto: links, static HTML forms posting to third-party services (Formspree, Netlify Forms), or display-only fake forms used as visual mockups.

teamniteo By teamniteo schedule Updated 5/20/2026

name: section-form description: Adds forms to Astro pages using React with Zod validation, submitting to a Cloudflare Workers handler. Use when user requests "add form", "contact form", "newsletter signup", "inquiry form", "booking form", "feedback form", "registration form", "waitlist", or any form functionality requiring client-side validation and server submission. Skip for pure mailto: links, static HTML forms posting to third-party services (Formspree, Netlify Forms), or display-only fake forms used as visual mockups.

Form Section

Adds accessible, validated forms using React state + Zod. Forms work as React islands with client:load. Submits to /~/form-{name} worker endpoint.

Why this stack

  • React island over native HTML form: Zod schemas + reactive state give per-field error rendering, conditional fields, and on-blur validation that vanilla forms can't express without manual DOM wiring. The island is small (~5KB gzipped), hydrates only when the form is in scope, and the rest of the page stays static.
  • Zod over native browser validation: native required/pattern/type=email checks scatter rules across HTML attributes, give browser-localized error strings, and don't share types with the worker. Zod is a single typed source of truth for both client and server, with custom messages and composable schemas.
  • Cloudflare Workers handler over mailto: or third-party services: a worker route validates submissions server-side (mailto skips validation entirely), enables anti-spam (rate-limiting, honeypots, Turnstile), structures payloads for downstream systems (CRM, email, DB), and keeps secrets out of the client. mailto also leaks the recipient's address and depends on the user having a mail client configured.

Workflow

Skip redundant installs. Steps 1–2 invoke bun add and bunx shadcn. Always check first: skip the install if the package is already in package.json (Step 1) or if the shadcn primitive already exists in src/components/ui/ (Step 2).

  1. Install deps: bun add zod sonner — first read package.json; skip if both are already listed
  2. Install shadcn: bunx --bun shadcn@latest add input textarea label button --overwrite — first check src/components/ui/; only add the primitives that are missing
  3. Create src/components/ui/field.tsx (see assets/field.tsx - adapt styling)
  4. Create worker/form.js handler (see assets/form.js - adapt logic)
  5. Register route in worker/index.js
  6. Create form component (see assets/ContactForm.tsx - adapt fields)
  7. Add to page with client:load (above-the-fold) or client:visible (below-the-fold, e.g. footer newsletter — defers hydration until the form scrolls into view, saving initial JS work)

Worker Setup

Create worker/form.js based on assets/form.js, then register in worker/index.js:

import { handleForm } from './form.js';

const ROUTES = {
  // ... existing routes
  '/~/form-': { handler: handleForm, description: 'Form Handler' },
};

Usage in Astro

---
import { ContactForm } from "@/components/ContactForm"
---
<section class="py-16 px-4">
  <div class="max-w-md mx-auto">
    <h2 class="text-3xl font-bold text-center mb-8">Get in Touch</h2>
    <ContactForm client:load />
  </div>
</section>

For below-the-fold forms (newsletter signup in the footer, contact form near page bottom), prefer client:visible over client:load — hydration is deferred until the form scrolls into view, which trims initial JS execution without harming UX.

Field Pattern

For additional fields, use standard React controlled inputs with the Field UI components:

const [values, setValues] = useState({ fieldName: "" })
const [errors, setErrors] = useState<Record<string, string>>({})

<Field data-invalid={!!errors.fieldName}>
  <FieldLabel htmlFor="fieldName">Label</FieldLabel>
  <Input
    id="fieldName"
    value={values.fieldName}
    onBlur={() => validateField("fieldName")}
    onChange={(e) => setValues(v => ({ ...v, fieldName: e.target.value }))}
    aria-invalid={!!errors.fieldName}
  />
  {errors.fieldName && <FieldError message={errors.fieldName} />}
</Field>

Additional Field Types

Select: bunx --bun shadcn@latest add select --overwrite (skip if src/components/ui/select.tsx already exists)

<Select value={values.option} onValueChange={(v) => setValues(s => ({ ...s, option: v }))}>
  <SelectTrigger><SelectValue placeholder="Select" /></SelectTrigger>
  <SelectContent><SelectItem value="opt1">Option</SelectItem></SelectContent>
</Select>

Checkbox: bunx --bun shadcn@latest add checkbox --overwrite (skip if src/components/ui/checkbox.tsx already exists)

<Field orientation="horizontal">
  <Checkbox
    checked={values.agree}
    onCheckedChange={(v) => setValues(s => ({ ...s, agree: !!v }))}
  />
  <FieldLabel>I agree to terms</FieldLabel>
</Field>

Validation

Zod schemas validate on submit and optionally on blur:

const schema = z.object({
  name: z.string().min(2, "Name required"),
  email: z.string().email("Invalid email"),
})

// Validate single field (for onBlur)
function validateField(name: string) {
  const fieldSchema = schema.shape[name]
  const result = fieldSchema.safeParse(values[name])
  setErrors(e => ({
    ...e,
    [name]: result.success ? "" : result.error.errors[0].message,
  }))
}

// Validate all fields (for onSubmit)
function validateAll() {
  const result = schema.safeParse(values)
  if (!result.success) {
    const fieldErrors: Record<string, string> = {}
    result.error.errors.forEach(e => {
      if (e.path[0]) fieldErrors[String(e.path[0])] = e.message
    })
    setErrors(fieldErrors)
    return false
  }
  setErrors({})
  return true
}

Accessibility

  • aria-invalid on inputs
  • data-invalid on Field wrapper
  • role="alert" on errors
  • htmlFor/id associations
  • autoComplete attributes

Assets

  • assets/field.tsx - Field components (Field, FieldLabel, FieldError, etc.)
  • assets/form.js - Worker handler example
  • assets/ContactForm.tsx - Complete contact form example
Install via CLI
npx skills add https://github.com/teamniteo/hakuto --skill section-form
Repository Details
star Stars 2
call_split Forks 1
navigation Branch main
article Path SKILL.md
More from Creator