Auf dieser Seite
Forms & analytics
One canonical contact form, built only from the brand component classes — and the
analytics wiring that turns it into a measurable funnel. The pattern is lifted from
the website's Digital-Check quiz: a tiny track() helper, an
ev_<context>_<action> naming convention, a strict
no-PII rule, and conversion events that fire only on confirmed success.
Full contract & agent coaching → docs/forms-and-analytics.md.
Live form
A standalone page that loads only tokens.css + css/base.css + components.css + utilities.css
— exactly what a consumer gets. The analytics run live inside the frame; open it full-screen
and watch window.dataLayer in the console as you interact. Source:
site/archetypes/contact-form.html — copy it as your starting point.
Markup
Built entirely from existing brand classes — .form-input,
.form-textarea, .form-checkbox, .btn-primary, and
.form-message (toggled --success/--error). No bespoke styling.
View source
<form id="contact-form" novalidate>
<label class="form-label" for="cf-name">Name *</label>
<input class="form-input" id="cf-name" name="name" type="text" autocomplete="name" required placeholder="Vor- und Nachname">
<label class="form-label" for="cf-email">E-Mail *</label>
<input class="form-input" id="cf-email" name="email" type="email" autocomplete="email" required placeholder="ihre@email.de">
<label class="form-label" for="cf-message">Nachricht *</label>
<textarea class="form-textarea" id="cf-message" name="message" required placeholder="Worum geht es?"></textarea>
<label class="form-checkbox">
<input type="checkbox" id="cf-consent" name="consent" required>
<span>Ich stimme der Verarbeitung zu. <a href="/datenschutz/">Datenschutz</a>.</span>
</label>
<button class="btn-primary" type="submit" id="cf-submit">Nachricht senden →</button>
<div class="form-message" id="cf-message-box" role="status" aria-live="polite" style="display:none;"></div>
</form>
Analytics JS
The part worth copying. One helper pushes flat events to
window.dataLayer; GTM (Consent Mode v2) decides downstream whether a tag fires —
we never gate here. The full file (with the backend stub + HubSpot example) is in the archetype source.
var FORM_NAME = 'contact';
var FORM_LOCATION = 'archetype'; // e.g. 'footer', 'kontakt-page', 'hero'
function track(eventName, params) {
try {
window.dataLayer = window.dataLayer || [];
var payload = { event: eventName };
if (params) {
for (var k in params) {
if (Object.prototype.hasOwnProperty.call(params, k)) payload[k] = params[k];
}
}
window.dataLayer.push(payload);
} catch (e) { /* analytics must never break the form */ }
}
// 1. VIEW — form rendered (use an IntersectionObserver for below-the-fold forms).
track('ev_contact_view', { form_name: FORM_NAME, form_location: FORM_LOCATION });
// 2. FIELD-START — first interaction, fired exactly once.
var started = false;
form.addEventListener('focusin', function () {
if (started) return;
started = true;
track('ev_contact_field_start', { form_name: FORM_NAME, form_location: FORM_LOCATION });
});
// 3. SUBMIT (success-gated) — fires only after the backend confirms. No PII.
submitForm(fields)
.then(function () {
track('ev_contact_submit', { form_name: FORM_NAME, form_location: FORM_LOCATION });
})
// 4. SUBMIT-ERROR — structural error_type only, never the error message.
.catch(function () {
track('ev_contact_submit_error', { form_name: FORM_NAME, form_location: FORM_LOCATION, error_type: 'network' });
});
Event table
The four-event lifecycle. Every payload is flat
({ event, …params }) and carries structural data only.
| Event | When it fires | Params |
|---|---|---|
ev_contact_view |
Form rendered / scrolled into view | form_name, form_location |
ev_contact_field_start |
First field focus — fired once | form_name, form_location |
ev_contact_submit |
Backend confirmed success (the conversion) | form_name, form_location |
ev_contact_submit_error |
Validation failed or backend rejected | form_name, form_location, error_type |
The no-PII rule
form_name,
form_location, field_name, error_type. This keeps
analytics free of personal data and the consent story clean.
DSGVO checklist — before you ship a new event
The process, generalized. Project-specific GTM wiring (real tag IDs,
triggers, CSP) lives in the website's docs/gtm-setup.md — this is the checklist
that travels with the pattern.
- The event carries no PII — only structural params.
- Its purpose is named in the right consent category (Analyse or Marketing) in the cookie-consent copy, and the cookie table in the Datenschutz page has a row for any storage it sets.
- If the receiving vendor is non-EU and not DPF-certified, the Drittland transfer is disclosed.
- The GTM tag has the matching consent requirement ("require additional consent to fire").
- The CSP allows any new script/endpoint the tag introduces.
Design events for YOUR form
Same skeleton, any form. Walk it in order:
- Name the context — one lowercase token (
contact,newsletter,booking). Every event isev_<context>_<action>. - Enumerate the lifecycle —
_view→_field_start→_submit→_submit_error. Add steps only if the form has them (a multi-step form adds_section_complete, like the quiz). - Choose params — structural only.
form_locationto tell instances apart;error_typeto bucket failures. Never a value the user typed. - Success-gate the conversion — fire
_submitinside the backend's success branch, not on click. A click that fails is not a conversion. - Run the DSGVO checklist above, then register the GTM trigger/tag.
Copy site/archetypes/contact-form.html and adapt — it
already wires all four events the right way.