ContactForm Forms
Contactformulier met async submit. Werkt met Formspree, Netlify Forms of eigen API.
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