Skip to content
Velocity X Docs
← All components

Contact Form — Simple

Free · MIT

Single-column name/email/subject/message contact form with a honeypot field, Netlify Forms opt-in, and a presentational action prop.

Category
Forms
Framework
astro
Version
1.0.0
Peer deps
none

Install

bash
velocityx add contact-form-simple

Free and MIT licensed — no account needed. The file lands under src/components/velocity/contact-form-simple/ by default (override with --dest), and it's yours to edit from the moment it's written.

Demo

View the live demo on the showcase site ↗

Source

Pulled live from the registry — this is the exact file velocityx add contact-form-simple writes into your project right now.

components/velocity/contact-form-simple/ContactFormSimple.astro

astro
---
//    VELOCITY COMPONENT · theming
// 
//    Colours here are Tailwind `primary-*` / `accent-*`, which resolve through
//    the Velocity preset. Out of the box they are a neutral indigo — NOT the
//    Aidxn brand — so this drops into your project without dragging our look in.
// 
//    ⚡ NOT USING VELOCITY? Run this once, in your project root:
// 
//          velocityx theme
// 
//    It writes velocity-preset.cjs + velocity-theme.css. Add the preset to your
//    tailwind.config, import the css, then set your whole brand in ONE place:
// 
//          :root {
//            --vx-brand: custom;
//            --vx-primary-600: 220 38 38;   <- your action colour
//            --vx-primary-700: 185 28 28;
//          }
// 
//    ⚠️  Raw RGB channels, space separated — NOT hex. `#DC2626` here silently
//        breaks every /opacity variant in the library (bg-primary-600/20 etc).
// 
//    Prefer not to install anything? Find-and-replace `primary-` with your own
//    scale name. Nothing else in this file depends on Velocity.
/**
 * ContactFormSimple — a single-column contact form: name, email, subject,
 * message. Presentational only — pass `action` for a form endpoint, or
 * `netlify` to wire up Netlify Forms. No client JS, no dependencies.
 *
 * Usage:
 *   <ContactFormSimple action="/api/contact" />
 *   <ContactFormSimple netlify formName="contact" />
 */
export interface Props {
  action?: string;
  method?: string;
  netlify?: boolean;
  formName?: string;
  heading?: string;
  subheading?: string;
  submitLabel?: string;
  className?: string;
}

const {
  action = '#',
  method = 'POST',
  netlify = false,
  formName = 'contact',
  heading = "Let's talk",
  subheading = 'Tell me a bit about your project and I will get back to you within one business day.',
  submitLabel = 'Send message',
  className = '',
} = Astro.props;

const inputClass =
  'w-full rounded-xl border-2 border-primary-200 dark:border-primary-800 bg-white dark:bg-primary-950/60 px-4 py-3 text-sm sm:text-base font-medium text-primary-950 dark:text-white placeholder:text-primary-950/40 dark:placeholder:text-primary-300/40 outline-none transition focus:border-primary-500 focus:ring-4 focus:ring-primary-500/15';

const labelClass = 'block text-sm font-semibold text-primary-950 dark:text-white mb-1.5';
---

<div class:list={['rounded-3xl border border-primary-500/15 bg-white/95 dark:bg-primary-950/60 backdrop-blur-sm p-6 sm:p-9 shadow-[0_8px_50px_rgba(0,0,0,0.08)]', className]}>
  <div class="mb-6 sm:mb-8">
    <h2 class="h-sub font-bold text-primary-950 dark:text-white">{heading}</h2>
    <p class="mt-2 body-md text-primary-950/60 dark:text-primary-200/60 max-w-lg">{subheading}</p>
  </div>

  <form
    action={action}
    method={method}
    name={netlify ? formName : undefined}
    data-netlify={netlify ? 'true' : undefined}
    class="flex flex-col gap-5"
  >
    {netlify && <input type="hidden" name="form-name" value={formName} />}

    {/* Honeypot — kept off-screen, not display:none, so screen readers skip it but it still traps bots. */}
    <p class="absolute -left-[9999px] w-px h-px overflow-hidden" aria-hidden="true">
      <label for="cfs-company">Company</label>
      <input id="cfs-company" name="company" type="text" tabindex={-1} autocomplete="off" />
    </p>

    <div class="grid grid-cols-1 sm:grid-cols-2 gap-5">
      <div>
        <label for="cfs-name" class={labelClass}>Full name</label>
        <input id="cfs-name" name="name" type="text" autocomplete="name" required class={inputClass} placeholder="Jordan Lee" />
      </div>
      <div>
        <label for="cfs-email" class={labelClass}>Email address</label>
        <input id="cfs-email" name="email" type="email" autocomplete="email" required class={inputClass} placeholder="jordan@company.com" />
      </div>
    </div>

    <div>
      <label for="cfs-subject" class={labelClass}>Subject</label>
      <input id="cfs-subject" name="subject" type="text" class={inputClass} placeholder="What's this about?" />
    </div>

    <div>
      <label for="cfs-message" class={labelClass}>Message</label>
      <textarea id="cfs-message" name="message" rows="5" required class={`${inputClass} resize-y`} placeholder="Share a few details…"></textarea>
    </div>

    <div class="flex items-center justify-between gap-4 flex-wrap">
      <button type="submit" class="btn btn-primary">{submitLabel}</button>
      <p class="body-sm text-primary-950/50 dark:text-primary-200/50">No spam. I reply personally.</p>
    </div>
  </form>
</div>

Theming

Uses the shared primary-* / accent-* scale — see Theming if colours look unstyled after install.