Zoeken...  ⌘K GitHub

ContactForm Forms

Contactformulier met async submit. Werkt met Formspree, Netlify Forms of eigen API.

/contact-form
src/components/forms/ContactForm.astro
---
/**
 * ContactForm
 * Contactformulier met naam, e-mail, bericht. Client-side validatie.
 * Submit via action URL (Formspree, Netlify Forms, eigen endpoint).
 *
 * Props:
 * - headline?: string
 * - sub?: string
 * - action: string — form action URL
 * - method?: 'POST' | 'GET'
 * - successMessage?: string
 * - fields?: ('phone' | 'company' | 'subject')[] — extra optionele velden
 * - netlify?: boolean — Netlify Forms support
 */
interface Props {
  headline?: string;
  sub?: string;
  action: string;
  method?: 'POST' | 'GET';
  successMessage?: string;
  fields?: ('phone' | 'company' | 'subject')[];
  netlify?: boolean;
}

const {
  headline = 'Stuur een bericht',
  sub,
  action,
  method = 'POST',
  successMessage = 'Bedankt! We nemen zo snel mogelijk contact op.',
  fields = [],
  netlify = false,
} = Astro.props;
---

<section class="contact-form">
  <div class="contact-form__inner">
    {(headline || sub) && (
      <div class="contact-form__header">
        {headline && <h2 class="contact-form__headline">{headline}</h2>}
        {sub && <p class="contact-form__sub">{sub}</p>}
      </div>
    )}

    <form
      class="contact-form__form"
      action={action}
      method={method}
      data-contact-form
      {...(netlify ? { 'data-netlify': 'true', 'netlify-honeypot': 'bot-field' } : {})}
    >
      {netlify && <input type="hidden" name="bot-field" />}

      <div class="contact-form__row">
        <label class="contact-form__label" for="cf-name">Naam <span aria-hidden="true">*</span></label>
        <input
          class="contact-form__input"
          type="text"
          id="cf-name"
          name="name"
          autocomplete="name"
          required
          placeholder="Jan de Vries"
        />
      </div>

      <div class="contact-form__row">
        <label class="contact-form__label" for="cf-email">E-mail <span aria-hidden="true">*</span></label>
        <input
          class="contact-form__input"
          type="email"
          id="cf-email"
          name="email"
          autocomplete="email"
          required
          placeholder="jan@bedrijf.nl"
        />
      </div>

      {fields.includes('phone') && (
        <div class="contact-form__row">
          <label class="contact-form__label" for="cf-phone">Telefoon</label>
          <input class="contact-form__input" type="tel" id="cf-phone" name="phone" autocomplete="tel" placeholder="+31 6 12345678" />
        </div>
      )}

      {fields.includes('company') && (
        <div class="contact-form__row">
          <label class="contact-form__label" for="cf-company">Bedrijf</label>
          <input class="contact-form__input" type="text" id="cf-company" name="company" autocomplete="organization" placeholder="Bedrijfsnaam BV" />
        </div>
      )}

      {fields.includes('subject') && (
        <div class="contact-form__row">
          <label class="contact-form__label" for="cf-subject">Onderwerp</label>
          <input class="contact-form__input" type="text" id="cf-subject" name="subject" placeholder="Waar gaat het over?" />
        </div>
      )}

      <div class="contact-form__row">
        <label class="contact-form__label" for="cf-message">Bericht <span aria-hidden="true">*</span></label>
        <textarea
          class="contact-form__input contact-form__input--textarea"
          id="cf-message"
          name="message"
          required
          rows="5"
          placeholder="Vertel ons meer..."
        ></textarea>
      </div>

      <div class="contact-form__submit">
        <button type="submit" class="contact-form__btn">
          <span class="contact-form__btn-label">Verstuur bericht</span>
          <span class="contact-form__btn-loading" aria-hidden="true">Bezig...</span>
        </button>
      </div>

      <div class="contact-form__success" aria-live="polite" hidden>
        <p>{successMessage}</p>
      </div>
    </form>
  </div>
</section>

<style>
  .contact-form { padding: 5rem 1.5rem; background: var(--color-bg); }
  .contact-form__inner { max-width: 640px; margin: 0 auto; }
  .contact-form__header { margin-bottom: 2.5rem; }
  .contact-form__headline {
    font-size: clamp(1.5rem, 3vw, 2rem);
    font-weight: 800;
    letter-spacing: -0.03em;
    margin: 0 0 0.5rem;
  }
  .contact-form__sub { font-size: 1rem; color: var(--color-muted); margin: 0; line-height: 1.6; }
  .contact-form__form { display: flex; flex-direction: column; gap: 1.25rem; }
  .contact-form__row { display: flex; flex-direction: column; gap: 0.375rem; }
  .contact-form__label {
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--color-text);
  }
  .contact-form__label span { color: var(--color-accent); }
  .contact-form__input {
    width: 100%;
    padding: 0.75rem 1rem;
    border: 1px solid color-mix(in srgb, var(--color-text) 20%, transparent);
    border-radius: var(--radius);
    background: var(--color-bg);
    color: var(--color-text);
    font-size: 1rem;
    font-family: inherit;
    transition: border-color 0.2s, box-shadow 0.2s;
    box-sizing: border-box;
  }
  .contact-form__input:focus {
    outline: none;
    border-color: var(--color-accent);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-accent) 20%, transparent);
  }
  .contact-form__input--textarea { resize: vertical; min-height: 120px; }
  .contact-form__submit { margin-top: 0.5rem; }
  .contact-form__btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    width: 100%;
    padding: 0.875rem;
    background: var(--color-accent);
    color: #fff;
    border: none;
    border-radius: var(--radius);
    font-weight: 700;
    font-size: 1rem;
    cursor: pointer;
    transition: opacity 0.2s;
    font-family: inherit;
  }
  .contact-form__btn:hover { opacity: 0.85; }
  .contact-form__btn[data-loading] .contact-form__btn-label { display: none; }
  .contact-form__btn:not([data-loading]) .contact-form__btn-loading { display: none; }
  .contact-form__success {
    padding: 1rem;
    background: color-mix(in srgb, #22c55e 15%, transparent);
    border: 1px solid #22c55e;
    border-radius: var(--radius);
    color: #15803d;
    text-align: center;
    font-weight: 600;
  }
  .contact-form__success p { margin: 0; }
</style>

<script>
  const form = document.querySelector('[data-contact-form]') as HTMLFormElement;
  const btn = form?.querySelector('.contact-form__btn') as HTMLButtonElement;
  const success = form?.querySelector('.contact-form__success') as HTMLElement;

  form?.addEventListener('submit', async (e) => {
    e.preventDefault();
    btn.setAttribute('data-loading', '');
    btn.disabled = true;

    try {
      const res = await fetch(form.action, {
        method: form.method,
        body: new FormData(form),
        headers: { Accept: 'application/json' },
      });
      if (res.ok) {
        form.reset();
        success.hidden = false;
        form.querySelectorAll('.contact-form__row').forEach(r => (r as HTMLElement).style.display = 'none');
        btn.style.display = 'none';
      } else {
        throw new Error('Server error');
      }
    } catch {
      btn.removeAttribute('data-loading');
      btn.disabled = false;
      alert('Er ging iets mis. Probeer het opnieuw of mail ons direct.');
    }
  });
</script>

Props

Prop Type Default Beschrijving
action * string Form submit URL (Formspree, Netlify, eigen endpoint)
headline string 'Stuur een bericht' Formulier titel
sub string Ondertitel
fields ('phone' | 'company' | 'subject')[] [] Extra optionele velden
netlify boolean false Netlify Forms support
successMessage string Bevestigingstekst na submit

* = verplicht