Formulaires
Système de formulaires complet : multi-steps, logique conditionnelle, champs custom, validation, toasts, webhooks.
Structure de base
<form class="form" data-form-webhook="https://votre-webhook.com"
data-form-redirect="/merci"
data-form-success="Merci !"
data-form-error="Erreur, réessayez.">
<div class="form__field form__field--float">
<input type="text" name="nom" class="form__input"
placeholder=" " data-validate="required|min:2">
<label class="form__label">Nom</label>
</div>
<div class="form__nav">
<button type="submit" class="form__submit">Envoyer</button>
</div>
</form>
| Attribut | Description |
|---|---|
data-form-webhook | URL(s) du webhook (POST JSON) — une ou plusieurs URLs séparées par des virgules |
data-form-redirect | URL de redirection après succès |
data-form-success | Message toast de succès |
data-form-error | Message toast d'erreur |
Pour les débutants : un webhook est une URL qui reçoit des données quand quelque chose se passe. Quand quelqu'un soumet votre formulaire, les données sont envoyées à cette URL (généralement un service d'automatisation comme Zapier, n8n, etc.) qui envoie ensuite un email avec les réponses.
Labels flottants
Ajoutez form__field--float pour activer l'animation de label flottant.
Le label se place dans le champ quand il est vide, et monte en haut quand le champ est focus ou rempli.
Important : l'input doit être placé avant le label dans le HTML, et avoir placeholder=" " (espace).
<!-- Label flottant : input AVANT label, placeholder=" " obligatoire -->
<div class="form__field form__field--float">
<input type="text" name="nom" class="form__input"
placeholder=" " data-validate="required|min:2">
<label class="form__label">Nom complet</label>
</div>
<!-- Textarea flottant -->
<div class="form__field form__field--float">
<textarea name="message" class="form__textarea"
placeholder=" "></textarea>
<label class="form__label">Votre message</label>
</div>
Structure complète d'un champ
<div class="form__field form__field--float">
<input type="text" name="nom" class="form__input"
placeholder=" " data-validate="required|min:2">
<label class="form__label">Nom complet</label>
<span class="form__label-hint">Optionnel</span>
<div class="form__error"></div>
</div>
| Classe | Description |
|---|---|
form__field--float | Active le label flottant sur le champ |
form__label-hint | Texte d'indication sous le label (ex : « Optionnel », « Si vous avez déjà un site ») |
form__error | Conteneur du message d'erreur de validation (rempli automatiquement par JS) |
- Le
placeholder=" "(espace) est nécessaire pour le sélecteur CSS:placeholder-shown. Si oublié, le JS l'ajoute automatiquement. Pourquoi un espace invisible ? Parce que le CSS utilise:placeholder-shownpour détecter si le champ est vide. Sans placeholder (même un espace), le navigateur ne peut pas savoir si le champ est vide, et le label flottant ne fonctionnerait pas. - L'input doit être avant le label dans le DOM (ordre inversé).
- Le
form__errorest optionnel : s'il est présent, le message d'erreur y sera injecté. Sinon, il est créé automatiquement. - Compatible avec la validation, les états d'erreur, et le pré-remplissage par URL.
- Pour les champs custom (select, radio, checkbox), le label classique au-dessus est recommandé.
Multi-Steps
Les formulaires multi-steps permettent de découper un long formulaire en plusieurs étapes séquentielles. Chaque étape est un <div class="form__step">. La navigation est linéaire : étape 1 → 2 → 3, etc.
Fonctionnement
.form__nextpasse à l'étape suivante (avec validation des champs de l'étape courante).form__prevrevient à l'étape précédente- Les champs
requireddes étapes non actives sont automatiquement désactivés (pour ne pas bloquer la validation native du navigateur) - Les transitions entre étapes sont animées (fade + translate)
- Le paramètre URL
?step=NomDuSteppermet de démarrer directement sur une étape spécifique (la valeur correspond audata-step-label, insensible à la casse)
Structure HTML
<form class="form" data-form-webhook="https://webhook.example.com/xxx"
data-form-success="Merci !"
data-form-error="Une erreur est survenue.">
<!-- Indicateurs d'étapes (générés automatiquement) -->
<div class="form__step-indicators"></div>
<!-- Barre de progression (optionnelle) -->
<div class="form__progress"><div class="form__progress-bar"></div></div>
<!-- Étape 1 -->
<div class="form__step form__step--active" data-step-label="Vous">
<div class="form__fields">
<div class="form__group form__group--half">
<label class="form__label">Prénom</label>
<input class="form__input" type="text" name="prenom" data-validate="required">
</div>
<div class="form__group form__group--half">
<label class="form__label">Nom</label>
<input class="form__input" type="text" name="nom" data-validate="required">
</div>
</div>
<div class="form__actions">
<button type="button" class="btn btn--primary form__next">
Suivant <span data-icon="arrow-right" data-icon-size="18"></span>
</button>
</div>
</div>
<!-- Étape 2 -->
<div class="form__step" data-step-label="Projet">
<div class="form__group">
<label class="form__label">Décrivez votre projet</label>
<textarea class="form__textarea" name="description" data-validate="required"></textarea>
</div>
<div class="form__actions">
<button type="button" class="btn btn--secondary form__prev">
<span data-icon="arrow-left" data-icon-size="18"></span> Précédent
</button>
<button type="button" class="btn btn--primary form__next">
Suivant <span data-icon="arrow-right" data-icon-size="18"></span>
</button>
</div>
</div>
<!-- Étape 3 -->
<div class="form__step" data-step-label="Envoi">
<div class="form__group">
<label class="form__label">Message complémentaire</label>
<textarea class="form__textarea" name="message"></textarea>
</div>
<div class="form__actions">
<button type="button" class="btn btn--secondary form__prev">
<span data-icon="arrow-left" data-icon-size="18"></span> Précédent
</button>
<button type="submit" class="btn btn--primary form__submit">
Envoyer <span data-icon="paper-airplane" data-icon-size="18"></span>
</button>
</div>
</div>
</form>
Indicateurs et barre de progression
.form__step-indicators: conteneur vide, les onglets sont générés automatiquement à partir desdata-step-labelde chaque step. L'indicateur actif reçoit la classeform__step-indicator--active, les étapes passées reçoiventform__step-indicator--done..form__progress: conteneur de la barre de progression. Contient.form__progress-bardont la largeur est mise à jour automatiquement (en %) selon l'étape courante.
Deux modes de boutons sont supportés : globaux (hors des steps) ou per-step (dans chaque step).
Mode boutons globaux
<form class="form" data-form-webhook="...">
<div class="form__step-indicators"></div>
<div class="form__progress">
<div class="form__progress-bar"></div>
</div>
<div class="form__step" data-step-label="Infos">
<!-- Champs step 1 -->
</div>
<div class="form__step" data-step-label="Projet">
<!-- Champs step 2 -->
</div>
<div class="form__nav">
<button type="button" class="form__prev">Précédent</button>
<button type="button" class="form__next">Suivant</button>
<button type="submit" class="form__submit">Envoyer</button>
</div>
</form>
Mode boutons per-step
Chaque step contient ses propres boutons avec des labels personnalisés :
<form class="form" data-form-webhook="...">
<div class="form__step-indicators"></div>
<div class="form__progress"><div class="form__progress-bar"></div></div>
<div class="form__step" data-step-label="Infos">
<!-- Champs -->
<div class="form__actions">
<button type="button" class="btn btn--primary form__next">Commencer</button>
</div>
</div>
<div class="form__step" data-step-label="Projet">
<!-- Champs -->
<div class="form__actions">
<button type="button" class="btn btn--secondary form__prev">Retour</button>
<button type="button" class="btn btn--primary form__next">Continuer</button>
</div>
</div>
<div class="form__step" data-step-label="Envoi">
<!-- Champs -->
<div class="form__actions">
<button type="button" class="btn btn--secondary form__prev">Retour</button>
<button type="submit" class="btn btn--primary form__submit">Envoyer ma demande</button>
</div>
</div>
</form>
Attributs des steps
| Attribut | Description |
|---|---|
data-step-label | Label affiché dans les indicateurs. Si absent, « Étape N » est utilisé par défaut. |
Comportement
- Barre de progression et indicateurs générés automatiquement
- Validation par step avant de passer au suivant
- Les champs
requireddes étapes non actives sont automatiquement désactivés - Transitions animées (fade + translate) entre les étapes
- Paramètre URL
?step=NomDuSteppour démarrer sur une étape spécifique - Le mode (global ou per-step) est détecté automatiquement
- Les deux modes de boutons (globaux et per-step) sont équivalents
Validation temps réel
La validation s'active en ajoutant data-validate sur un champ avec les règles séparées par |. Elle se déclenche au blur (quand l'utilisateur quitte le champ) et lors de la soumission ou du passage à l'étape suivante.
Règles disponibles
| Règle | Description |
|---|---|
required | Champ obligatoire |
email | Format email valide |
phone | Format téléphone valide |
url | Format URL (http/https) |
min:N | Minimum N caractères |
max:N | Maximum N caractères |
regex:/pattern/ | Regex personnalisé |
regex:/pattern/:Message | Regex avec message d'erreur personnalisé |
<input data-validate="required|email">
<input data-validate="required|min:3|max:50">
<input data-validate="phone">
<input data-validate="regex:/^[0-9]{5}$/:Code postal invalide (5 chiffres).">
<input data-validate="regex:/^[A-Z]{2}-[0-9]{3}-[A-Z]{2}$/:Format plaque invalide.">
Comportement
- Validation au blur : la validation se déclenche quand l'utilisateur quitte un champ (événement
blur), évitant les erreurs intempestives pendant la saisie. - Erreurs visuelles : un champ invalide reçoit une bordure rouge et un message d'erreur « Ce champ est obligatoire » ou le message personnalisé de la règle regex. Le message est injecté dans le
.form__errors'il existe, sinon il est créé automatiquement. Le wrapper.form__groupou.form__fieldle plus proche sert de conteneur pour l'état d'erreur. Les messages d'erreur possèdentrole="alert"etaria-live="assertive"pour être annoncés immédiatement par les lecteurs d'écran. - Toast d'erreur : si la validation échoue au clic sur Suivant ou Envoyer, un toast d'erreur s'affiche en plus des erreurs visuelles sur les champs.
- Support des champs custom : la validation fonctionne sur les custom selects, radio groups, checkbox groups et multiselects en plus des inputs natifs.
Regex — Syntaxe
Le format est regex:/pattern/ ou regex:/pattern/:Message d'erreur. Le pattern est une expression régulière JavaScript (sans flags). Si aucun message n'est fourni, le message par défaut est « Format invalide. ».
Exemples de patterns utiles :
| Pattern | Usage |
|---|---|
^[0-9]{5}$ | Code postal français |
^[0-9]{14}$ | SIRET |
^[0-9]{9}$ | SIREN |
^FR\d{2}[A-Z0-9]{23}$ | IBAN français |
^FR[0-9A-Z]{2}[0-9]{9}$ | N° TVA intracommunautaire |
^[A-Z]{2}-[0-9]{3}-[A-Z]{2}$ | Plaque d'immatriculation FR |
^[a-zA-ZÀ-ÿ\s'-]+$ | Lettres uniquement (accents inclus) |
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$ | Mot de passe fort |
Exemple complet avec validation
<form class="form" data-form-webhook="https://webhook.example.com/xxx">
<div class="form__fields">
<div class="form__group form__group--half">
<label class="form__label">Email</label>
<input class="form__input" type="email" name="email"
data-validate="required|email">
<div class="form__error"></div>
</div>
<div class="form__group form__group--half">
<label class="form__label">Téléphone</label>
<input class="form__input" type="tel" name="telephone"
data-validate="required|phone">
<div class="form__error"></div>
</div>
<div class="form__group">
<label class="form__label">Code postal</label>
<input class="form__input" type="text" name="cp"
data-validate="required|regex:/^[0-9]{5}$/:Code postal invalide.">
<div class="form__error"></div>
</div>
<!-- Validation sur un custom select -->
<div class="form__group">
<label class="form__label">Pays</label>
<div class="form__select" data-name="pays" data-validate="required">
<div class="form__select-trigger">Choisir...</div>
<div class="form__select-options">
<div class="form__select-option" data-value="fr">France</div>
<div class="form__select-option" data-value="be">Belgique</div>
</div>
<input type="hidden" name="pays" value="">
</div>
</div>
</div>
<div class="form__nav">
<button type="submit" class="form__submit">Envoyer</button>
</div>
</form>
Logique conditionnelle
Affichez/masquez des champs en fonction de la valeur d'un autre champ :
<!-- Champ de référence -->
<div class="form__radio-group" data-name="type">
<div class="form__radio-option" data-value="perso">Particulier</div>
<div class="form__radio-option" data-value="pro">Professionnel</div>
</div>
<!-- Visible uniquement si type = pro -->
<div class="form__field" data-condition="type=pro">
<label class="form__label">Entreprise</label>
<input name="entreprise" class="form__input">
</div>
Opérateurs
| Format | Description | Exemple |
|---|---|---|
champ=valeur | Égalité stricte | type=pro |
champ!=valeur | Différent de | type!=perso |
champ=val1,val2 | L’une des valeurs (OR implicite) | poste=designer-ui-ux,fullstack |
champ=* | Non vide (le champ a une valeur) | email=* |
champ>N | Supérieur à (comparaison numérique) | budget>5000 |
champ<N | Inférieur à (comparaison numérique) | budget<10000 |
Combinaisons (AND / OR)
| Syntaxe | Description |
|---|---|
cond1&&cond2 | AND : toutes les conditions doivent être vraies |
cond1||cond2 | OR : au moins une condition doit être vraie |
<!-- AND : visible si type=pro ET pays=fr -->
<div class="form__field" data-condition="type=pro&&pays=fr">
<!-- OR : visible si expérience confirmée OU senior -->
<div class="form__field" data-condition="experience=confirme-3-5-ans||experience=senior-5-ans">
Comportement
- Les champs masqués ne sont pas envoyés dans le webhook (ils sont exclus du payload)
- Le nom du champ dans la condition correspond à l’attribut
nameoudata-namedu champ référencé - Les conditions sont évaluées en temps réel à chaque changement de valeur
- Pour les champs custom (select, radio, checkbox, multiselect), la valeur comparée est le slug généré automatiquement à partir du texte de l’option (ex : « Designer UI/UX » →
designer-ui-ux)
Champs custom (sans dépendances)
Des éléments de formulaire 100% stylisables en CSS, remplaçant les éléments natifs difficiles à styler :
Custom Select
<div class="form__select" data-name="pays">
<div class="form__select-trigger" data-placeholder="Choisir un pays...">Choisir un pays...</div>
<div class="form__select-options">
<div class="form__select-option" data-value="fr">France</div>
<div class="form__select-option" data-value="be">Belgique</div>
<div class="form__select-option" data-value="ch">Suisse</div>
</div>
</div>
Custom Number
<div class="form__number">
<button type="button" class="form__number-minus">−</button>
<input type="number" name="qty" class="form__number-input"
value="1" min="1" max="100" step="1">
<button type="button" class="form__number-plus">+</button>
</div>
Custom Radio Group
<div class="form__radio-group" data-name="taille">
<div class="form__radio-option" data-value="s">S</div>
<div class="form__radio-option" data-value="m">M</div>
<div class="form__radio-option" data-value="l">L</div>
</div>
Option « Autre » (radio)
Ajoutez une option avec data-value="__other__" et un input form__other-input pour permettre une saisie libre :
<div class="form__radio-group" data-name="taille">
<div class="form__radio-option" data-value="s">S</div>
<div class="form__radio-option" data-value="m">M</div>
<div class="form__radio-option" data-value="l">L</div>
<div class="form__radio-option" data-value="__other__">Autre : <input type="text" class="form__other-input" placeholder="Précisez..."></div>
</div>
Quand l'option « Autre » est sélectionnée, la valeur envoyée est le texte saisi. L'input reçoit le focus automatiquement.
Custom Checkbox Group
<div class="form__checkbox-group" data-name="options">
<div class="form__checkbox-option" data-value="opt1">Option 1</div>
<div class="form__checkbox-option" data-value="opt2">Option 2</div>
<div class="form__checkbox-option" data-value="opt3">Option 3</div>
</div>
Option « Autre » (checkbox)
Même principe que pour les radios :
<div class="form__checkbox-group" data-name="options">
<div class="form__checkbox-option" data-value="opt1">Option 1</div>
<div class="form__checkbox-option" data-value="opt2">Option 2</div>
<div class="form__checkbox-option" data-value="__other__">Autre : <input type="text" class="form__other-input" placeholder="Précisez..."></div>
</div>
La valeur saisie remplace __other__ dans la liste des valeurs envoyées (ex : opt1,Mon texte perso).
Custom Date Picker
Sélecteur de date stylisé avec icône calendrier custom, masquant l'apparence native du navigateur :
<div class="form__date" data-name="date_rdv">
<input class="form__date-input" type="date" name="date_rdv" required>
</div>
- L'icône calendrier est affichée via CSS (pseudo-élément
::after) - Le picker natif du navigateur s'ouvre au clic
- Supporte la validation (
required,data-validate) - La valeur est au format ISO
YYYY-MM-DD
Custom Color Picker
Sélecteur de couleur avec aperçu swatch + affichage hexadécimal :
<div class="form__color" data-name="couleur">
<input class="form__color-input" type="color" name="couleur" value="#6366f1">
<span class="form__color-value">#6366F1</span>
</div>
- Le wrapper entier est cliquable (ouvre le picker natif)
- L'affichage hex se met à jour en temps réel
- État focus avec halo primaire sur le wrapper
Custom Range Slider
Slider avec piste de progression colorée, poignée stylisée et affichage de la valeur :
<div class="form__range" data-name="budget">
<input class="form__range-input" type="range" name="budget"
min="0" max="100" step="1" value="50">
<div class="form__range-labels">
<span>0</span>
<span class="form__range-value">50</span>
<span>100</span>
</div>
</div>
- La piste se remplit progressivement avec la couleur primaire
- La poignée grossit au clic (
grab→grabbing) - La valeur est mise à jour en temps réel
File Upload (upload de fichiers)
Champ d'upload de fichier avec zone de drag & drop intégrée.
<div class="form__file" data-name="cv">
<div class="form__file-dropzone">
<span data-icon="arrow-up-tray" data-icon-size="24"></span>
<span class="form__file-text">Glissez un fichier ici ou <strong>parcourir</strong></span>
<span class="form__file-hint">image/*,.pdf</span>
</div>
<input type="file" class="form__file-input" name="cv" accept="image/*,.pdf">
</div>
| Attribut | Description |
|---|---|
accept | Types de fichiers acceptés (ex : image/*, .pdf,.doc,.docx) |
multiple | Autorise l'upload de plusieurs fichiers |
Fonctionnement :
- Le drag & drop est supporté : glisser un fichier sur la zone affiche un feedback visuel (changement de bordure et d'opacité)
- Au drop ou à la sélection d'un fichier, le nom du fichier sélectionné est affiché dans la zone
- L'input file natif est superposé en transparent sur la zone (le clic ouvre le sélecteur natif)
- Le hint (
form__file-hint) affiche les types de fichiers acceptés sous la zone - Compatible avec le constructeur de formulaires (type « Fichier »)
- Les fichiers sont convertis en base64 et inclus dans le payload JSON au format
{ filename, data, type, size } - Limite de taille : 5 Mo par soumission (fichiers + champs combinés)
- Le proxy PHP
form.phpforward le payload complet (base64 inclus) au webhook
Exemple avec types restreints
<div class="form__file" data-name="brief">
<div class="form__file-dropzone">
<span data-icon="arrow-up-tray" data-icon-size="24"></span>
<span class="form__file-text">Glissez un fichier ici ou <strong>parcourir</strong></span>
<span class="form__file-hint">.pdf,.doc,.docx</span>
</div>
<input type="file" class="form__file-input" name="brief"
accept=".pdf,.doc,.docx">
</div>
Calculated Field
Champ calculé automatiquement à partir d'une formule référençant d'autres champs :
<div class="form__calculated" data-name="total"
data-formula="{prix} * {quantite}" data-decimals="2">
<span class="form__calculated-value">—</span>
<span class="form__calculated-suffix">€</span>
<input type="hidden" name="total">
</div>
| Attribut | Description |
|---|---|
data-formula | Expression mathématique avec {nom_champ} comme variables |
data-decimals | Nombre de décimales (défaut : 2) |
- Opérateurs supportés :
+,-,*,/, parenthèses() - Recalcul en temps réel à chaque changement
- Le résultat est stocké dans l'input hidden et envoyé avec le formulaire
Évaluation sécurisée : la formule est évaluée par un parseur récursif descent interne (
safeEval), sans recours àeval()ninew Function(). Seuls les nombres et les opérateurs+,-,*,/,()sont acceptés — toute expression contenant d'autres caractères retourneNaN. Cette approche est compatible avec les politiques CSP strictes (Content Security Policy).
HTML Libre
Bloc de contenu HTML libre inséré directement dans le formulaire, sans champ de saisie :
<div class="form__html">
<p>Texte informatif, séparateur, instructions...</p>
</div>
- Utilisé pour ajouter du contenu entre les champs
- N'envoie aucune donnée dans le webhook
- Le constructeur inclut une validation de syntaxe en temps réel
Icônes dans les options (radio / checkbox)
Ajoutez data-icon sur les options pour afficher une icône du framework (324 Heroicons disponibles). Contrôlez la position avec l'ordre dans le HTML :
<div class="form__radio-group" data-name="service">
<!-- Icône à gauche -->
<div class="form__radio-option" data-value="design">
<span data-icon="paint-brush" data-icon-size="18"></span> Design
</div>
<!-- Icône à droite -->
<div class="form__radio-option" data-value="seo">
SEO <span data-icon="magnifying-glass" data-icon-size="18"></span>
</div>
</div>
Icônes dans les boutons
Les boutons de formulaire peuvent aussi contenir des icônes :
<button type="submit" class="btn btn--primary form__submit">
<span data-icon="paper-airplane" data-icon-size="18"></span> Envoyer
</button>
Largeurs de champs (CSS Grid 6 colonnes)
Le système de layout repose sur une grille CSS Grid de 6 colonnes. Par défaut, chaque champ (form__group) prend 100% (span 6). Utilisez l'attribut data-field-width ou les classes CSS pour contrôler la largeur :
| Attribut | Colonnes | Largeur | Classe CSS |
|---|---|---|---|
data-field-width="1-1" (défaut) | span 6 | 100% | (aucune) |
data-field-width="1-2" | span 3 | 50% | form__group--half |
data-field-width="1-3" | span 2 | 33.33% | form__group--third |
Wrapper form__fields (obligatoire)
Le wrapper .form__fields est toujours obligatoire, même si tous les champs sont en pleine largeur. C'est lui qui porte la grille CSS Grid et les gaps entre les champs.
<div class="form__fields">
<div class="form__group form__group--half">
<label class="form__label">Prénom</label>
<input class="form__input" type="text" name="prenom">
</div>
<div class="form__group form__group--half">
<label class="form__label">Nom</label>
<input class="form__input" type="text" name="nom">
</div>
<div class="form__group form__group--third">...</div>
<div class="form__group form__group--third">...</div>
<div class="form__group form__group--third">...</div>
<div class="form__group">
<!-- Pleine largeur par défaut -->
</div>
</div>
Les champs en --half et --third passent en pleine largeur sur mobile (< 768px) par défaut.
Largeurs responsive par breakpoint
Pour un contrôle plus fin, utilisez les classes responsive par breakpoint :
| Classe | Breakpoint | Largeur |
|---|---|---|
form__group--tablet-full | ≤ 991px | 100% |
form__group--tablet-half | ≤ 991px | 50% |
form__group--tablet-third | ≤ 991px | 33.33% |
form__group--mobile-full | ≤ 767px | 100% |
form__group--mobile-half | ≤ 767px | 50% |
form__group--mobile-third | ≤ 767px | 33.33% |
form__group--sm-full | ≤ 478px | 100% |
form__group--sm-half | ≤ 478px | 50% |
form__group--sm-third | ≤ 478px | 33.33% |
<!-- Desktop: 1/3, Mobile: 1/2 -->
<div class="form__group form__group--third form__group--mobile-half" data-field-width="1-3">
<label class="form__label">Ville</label>
<input class="form__input" type="text" name="ville">
</div>
Par défaut, les champs en 1-2 et 1-3 passent en pleine largeur sur mobile (≤ 767px). Les classes --mobile-* permettent de forcer une largeur spécifique. Le constructeur de formulaires permet de définir ces largeurs visuellement.
Espacements via attributs
Les espacements du formulaire sont contrôlables via des attributs data-* sur la balise <form> :
| Attribut | Description | Valeurs |
|---|---|---|
data-form-gap | Gap entre les champs (grille) | xs, sm, md, base, lg, xl, 2xl, 3xl |
data-btn-gap | Gap entre les boutons d'action | xs, sm, md, base, lg, xl, 2xl, 3xl |
data-btn-margin | Marge au-dessus du bloc boutons | xs, sm, md, base, lg, xl, 2xl, 3xl |
Quand data-form-gap est défini, le margin-bottom par défaut des champs est automatiquement supprimé.
<form class="form"
data-form-webhook="https://webhook.example.com/xxx"
data-form-gap="lg"
data-btn-gap="md"
data-btn-margin="xl">
<div class="form__fields">
<!-- Les champs utilisent un gap "lg" -->
</div>
<div class="form__actions">
<!-- Gap "md" entre boutons, marge "xl" au-dessus -->
</div>
</form>
Soumission et redirection
Le comportement à la soumission dépend de la combinaison des attributs data-form-webhook et data-form-redirect :
| Webhook | Redirect | Comportement |
|---|---|---|
| ✓ | ✓ | Envoie le webhook, puis redirige avec les champs en query params |
| ✗ | ✓ | Redirige directement avec les champs en query params |
| ✓ | ✗ | Envoie le webhook et affiche le message de succès (toast) |
| ✗ | ✗ | Affiche le message de succès (toast) |
Query params de redirection
Quand une redirection est configurée (data-form-redirect), les query params ajoutés à l'URL de destination incluent :
- Tous les champs remplis du formulaire (nom, email, message, etc.)
- Les paramètres URL existants de la page source (UTMs, etc.)
https://merci.example.com?prenom=Manon&email=manon@ex.fr&utm_source=google
Cela permet à la page de destination d'exploiter les données du formulaire (personnalisation, tracking, etc.).
Exemple complet avec toutes les fonctionnalités
<form class="form"
data-form-webhook="https://webhook.example.com/xxx"
data-form-success="Merci !"
data-form-redirect="https://merci.example.com"
data-form-gap="lg"
data-btn-gap="md"
data-btn-margin="xl">
<div class="form__step-indicators"></div>
<div class="form__step" data-step-label="Identité">
<div class="form__fields">
<div class="form__group" data-field-width="1-2">
<label class="form__label">Prénom</label>
<input class="form__input" type="text" name="prenom" required>
</div>
<div class="form__group" data-field-width="1-2">
<label class="form__label">Nom</label>
<input class="form__input" type="text" name="nom" required>
</div>
<div class="form__group" data-field-width="1-3">
<label class="form__label">Ville</label>
<input class="form__input" type="text" name="ville">
</div>
<div class="form__group" data-field-width="1-3">
<label class="form__label">Code postal</label>
<input class="form__input" type="text" name="cp"
data-validate="regex:/^[0-9]{5}$/:Code postal invalide.">
</div>
<div class="form__group" data-field-width="1-3">
<label class="form__label">Pays</label>
<div class="form__select" data-name="pays" data-validate="required">
<div class="form__select-trigger">Choisir...</div>
<div class="form__select-options">
<div class="form__select-option" data-value="fr">France</div>
<div class="form__select-option" data-value="be">Belgique</div>
</div>
<input type="hidden" name="pays" value="">
</div>
</div>
</div>
<div class="form__actions">
<button type="button" class="btn btn--primary form__next">
Suivant <span data-icon="arrow-right" data-icon-size="18"></span>
</button>
</div>
</div>
<div class="form__step" data-step-label="Message">
<div class="form__fields">
<div class="form__group">
<label class="form__label">Votre message</label>
<textarea class="form__textarea" name="message" data-validate="required"></textarea>
</div>
</div>
<div class="form__actions">
<button type="button" class="btn btn--secondary form__prev">
<span data-icon="arrow-left" data-icon-size="18"></span> Précédent
</button>
<button type="submit" class="btn btn--primary form__submit">
Envoyer <span data-icon="paper-airplane" data-icon-size="18"></span>
</button>
</div>
</div>
</form>
Custom Multi Select
<div class="form__multiselect" data-name="competences">
<div class="form__multiselect-trigger" data-placeholder="Choisir...">Choisir...</div>
<div class="form__multiselect-options">
<div class="form__multiselect-option" data-value="html">HTML</div>
<div class="form__multiselect-option" data-value="css">CSS</div>
<div class="form__multiselect-option" data-value="js">JavaScript</div>
</div>
</div>
Accessibilité clavier : Tab pour focus, Entrée/Espace pour ouvrir/fermer, Flèches pour naviguer, Entrée pour toggler une option, Échap pour fermer. Attributs ARIA : role="combobox", aria-haspopup="listbox", aria-multiselectable="true".
Focus visible : tous les éléments de formulaire custom (select, radio, checkbox, multiselect, date, couleur, range, fichier) ont un anneau de focus visible au clavier (:focus-visible) pour une navigation accessible.
Webhook (payload)
Proxy sécurisé
Les URLs de webhook sont configurées directement dans l'attribut data-form-webhook de chaque formulaire. Plusieurs URLs sont supportées (séparées par des virgules) :
<!-- Un seul webhook -->
<form class="form" data-form-webhook="https://webhook.example.com/xxx">
<!-- Plusieurs webhooks -->
<form class="form" data-form-webhook="https://hook1.example.com/aaa, https://hook2.example.com/bbb">
À la soumission, forms.js lit les URLs depuis data-form-webhook et les envoie dans le corps du POST (champ _webhooks) vers le proxy /api/form.php. Le proxy ajoute la date au payload, puis forward vers chaque webhook. Les webhooks gèrent eux-mêmes les notifications (email, Slack, etc.).
Payload
Le formulaire envoie un POST JSON au webhook configuré. Le payload contient :
- Tous les champs visibles (respect de la logique conditionnelle)
date_now: date française (ex : « 2 Février 2026, 10h52 »)url: URL de la pageuser_agent: navigateur de l'utilisateurutm_source,utm_medium,utm_campaign,utm_term,utm_content: UTMs depuis l'URL
Exemple de payload
{
"nom": "Manon",
"email": "manon@exemple.fr",
"type_client": "entreprise",
"entreprise": "Mon Agence",
"projet": "site-vitrine",
"services": "design,dev",
"date_now": "26 Février 2026, 14h30",
"url": "https://monsite.fr/contact?utm_source=google",
"user_agent": "Mozilla/5.0...",
"utm_source": "google",
"utm_medium": "cpc"
}
Toasts
Les toasts sont automatiques (succès/erreur) mais utilisables aussi manuellement. Le conteneur de toasts possède role="status" et aria-live="polite" pour être annoncé par les lecteurs d’écran sans interrompre l’utilisateur.
// Afficher un toast manuellement
showToast('Message ici', 'success'); // success, error, warning, info
showToast('Attention !', 'warning', 6000); // durée en ms (défaut : 4000)
| Paramètre | Type | Défaut | Description |
|---|---|---|---|
message | string | — | Texte affiché |
type | string | 'info' | success, error, warning, info |
duration | number | 4000 | Durée en ms avant disparition |
État loading du bouton submit
Lors de la soumission, le bouton submit passe automatiquement en état loading : le contenu (texte + icône) est remplacé par une icône arrow-path animée (rotation) + « Envoi en cours... ». Le bouton est désactivé avec opacité réduite. Au retour (succès ou erreur), le contenu original est restauré.
La classe CSS form__spinner applique la rotation. Elle est ajoutée automatiquement par le JS.
Pré-remplissage par URL
Les paramètres d'URL correspondent aux attributs name des champs :
https://monsite.fr/contact?nom=Manon&email=manon@ex.fr&type_client=entreprise
Les inputs natifs, selects, radios et checkboxes sont pré-remplis automatiquement. Les champs custom (data-name) sont également supportés.
- Le formulaire ne s'envoie pas : vérifiez que l'attribut
data-form-webhookcontient une URL de webhook valide (ex :https://webhook.example.com/xxx). - La validation ne fonctionne pas : ajoutez l'attribut
data-validatesur chaque champ avec les règles séparées par|(ex :data-validate="required|email"). Le formulaire n'a pas besoin d'attribut spécial — le JS détecte automatiquement les champs portantdata-validate. - Le multi-step ne passe pas à l'étape suivante : chaque étape doit être un
<div class="form__step">et les boutons navigation doivent avoir les classes.form__next/.form__prev. - Le champ conditionnel ne s'affiche pas : vérifiez la syntaxe de
data-condition: le nom du champ doit correspondre à l'attributname(pas l'id). - Le toast ne s'affiche pas : appelez
showToast('message', 'success')depuis la console pour tester. Vérifiez queforms.cssest chargé.
Constructeur de formulaires (Configurateur)
Le Configurateur inclut un constructeur visuel de formulaires accessible depuis le panel Constructeurs → Formulaires. Il permet de créer des formulaires multi-steps complets sans écrire de code.
Interface
L'interface est divisée en trois zones :
| Zone | Position | Contenu |
|---|---|---|
| Étapes | Gauche | Liste des steps avec drag & drop pour réordonner |
| Éditeur visuel | Centre | Aperçu WYSIWYG du formulaire, glisser-déposer des champs |
| Panneau latéral | Droite | Propriétés du champ sélectionné, paramètres, aperçu live, code généré |
Cliquez sur un champ pour afficher son panneau de configuration à droite : label, name, placeholder, validation, design et logique conditionnelle.
Fonctionnalités
- Ajout de champs : cliquer sur le
+dans l'éditeur pour ajouter un champ (texte, email, téléphone, nombre, textarea, select, radio, checkbox, date, couleur, range, calculé, caché, HTML libre) - Configuration des champs : sélectionner un champ pour modifier son label, placeholder, nom, validation, largeur (1/1, 1/2, 1/3), logique conditionnelle, et option « Autre »
- Options des select/radio/checkbox : ajouter, supprimer et réordonner les options par glisser-déposer (poignée de drag sur chaque option)
- Icônes : ajouter des icônes Heroicons aux options et aux boutons, avec choix de position (gauche/droite)
- Multi-steps : ajouter des étapes, les réordonner par drag & drop
- Boutons personnalisés : ajouter des boutons prev/next/submit par étape avec texte, icône et logique conditionnelle
- Onglets de navigation : activables/désactivables dans les paramètres pour les formulaires multi-steps
- Undo/Redo : historique complet des modifications avec Ctrl+Z / Ctrl+Y
- Clic droit contextuel : dupliquer, supprimer, déplacer un champ via le menu contextuel
Drag & Drop
Le constructeur utilise le drag & drop à plusieurs niveaux :
- Champs : réordonner les champs dans l'éditeur visuel avec un indicateur bleu montrant la position de dépôt
- Étapes : réordonner les steps dans le panneau gauche avec indicateur bleu
- Options : réordonner les options des select/radio/checkbox dans le panneau de propriétés (glisser via la poignée
⠇)
Paramètres du formulaire
L'onglet paramètres (⚙) dans le panneau latéral permet de configurer :
- Webhook URL : endpoint pour l'envoi des données (
/api/form.phprecommandé) - Message de succès / erreur : textes affichés après envoi
- URL de redirection : page vers laquelle rediriger après succès
- Textes des boutons : personnaliser « Précédent », « Suivant », « Envoyer »
- Onglets de navigation : afficher ou masquer les onglets (formulaires multi-steps uniquement)
Code généré
L'onglet Code affiche le HTML généré en temps réel, prêt à copier-coller dans une page ou à embedder via data-form-embed.
Aperçu live
L'onglet Aperçu affiche un rendu fidèle du formulaire avec les styles du framework appliqués. Permet de tester la navigation multi-step, les conditions, les validations et les champs calculés sans quitter le builder.
Import / Export
- Exporter : menu
⋯d'un formulaire dans la vue grille → « Exporter ». Télécharge le fichier.html. - Importer : cliquer sur « Importer un formulaire » dans la toolbar. Le builder parse le HTML et reconstruit l'état complet (champs, steps, boutons, conditions, options, CSS/JS custom). Seuls les fichiers
.html/.htmsont acceptés.
Garde‑fous de l'import
L'import refuse explicitement (au lieu d'accepter silencieusement) les fichiers qui ne correspondent pas à la structure attendue. Un toast d'erreur s'affiche dans le builder avec la cause exacte.
| Cas | Réponse | Message |
|---|---|---|
Fichier > 1 Mo | 413 | Fichier trop volumineux — Taille reçue X octets, maximum autorisé 1048576 octets (1024 Ko) |
HTML sans form__step ni form__group | 400 | Aucune structure de formulaire détectée — Vérifiez qu'il s'agit bien d'un formulaire généré par Beely Framework. |
| Formulaire sans aucun champ | 400 | Formulaire vide — Le formulaire ne contient aucun champ détecté. |
| Plus de 100 étapes | 400 | Trop d'étapes — N étapes détectées, maximum 100. |
| Plus de 1000 champs | 400 | Trop de champs — N champs détectés, maximum 1000. |
| HTML invalide / non parsable | 400 | HTML invalide ou non parsable |
Sécurité : les balises <script>, <iframe> et attributs on* du HTML importé sont ignorés par le parser de structure (qui ne lit que les classes BEM form__*). Le HTML uploadé n'est jamais ré‑affiché tel quel : la structure parsée est immédiatement ré‑écrite via generateHTML() qui passe chaque valeur par escapeHtml(). Risque XSS nul même sur un fichier malveillant.
L'import retourne aussi un objet stats avec le nombre d'étapes, de champs et la taille du payload, affiché dans le toast de confirmation : « Formulaire importé (3 étape(s), 13 champ(s)) ».
Édition directe du HTML
Le HTML est la source de vérité. Le fichier .json est un cache du builder. Si le HTML est édité directement (VSCode, autre éditeur), le builder le détecte automatiquement à l'ouverture suivante (comparaison des timestamps) et re-parse le HTML. Pas besoin de mettre le JSON à jour manuellement.
Intégrer un formulaire dans une page (Embed)
Le système d'embed permet d'insérer un formulaire sauvegardé dans n'importe quelle page du site, sans copier-coller le HTML. Le formulaire est chargé dynamiquement depuis son fichier HTML dans forms/.
Utilisation
Ajoutez un conteneur avec l'attribut data-form-embed pointant vers le nom du formulaire, puis incluez le script form-embed.js :
<!-- Conteneur d'intégration -->
<div data-form-embed="candidature"></div>
<!-- Script d'embed (à charger après forms.js) -->
<script src="../core/js/form-embed.js" defer></script>
Le nom passé à data-form-embed correspond au nom du formulaire tel qu'il a été sauvegardé dans le constructeur (sans extension, en minuscules, espaces remplacés par des tirets).
Paramètre URL ?step=
Pour un formulaire multi-steps, le paramètre URL ?step=NomDuStep permet d'ouvrir directement une étape spécifique. La valeur correspond au data-step-label de l'étape (insensible à la casse).
https://monsite.fr/contact?step=Projet
Fonctionnement
Voici les étapes exécutées par le script d'embed :
- Détection : le script recherche tous les éléments
[data-form-embed]dans la page. - Chargement : pour chaque conteneur, il charge le fichier HTML correspondant depuis
forms/{nom}.htmlviafetch(), avec un timeout de 8 secondes. Si le chargement échoue ou dépasse le timeout, un message d'erreur s'affiche dans le conteneur. - Injection : le HTML est injecté dans le conteneur
data-form-embed. - Ré-initialisation : tous les modules JS sont ré-initialisés sur le contenu injecté —
initForms(),initIcons(),initElements(),initAnimations(),initTextAnimations()— pour que la validation, les icônes, les éléments interactifs et les animations fonctionnent sur le formulaire chargé dynamiquement. - Pré-remplissage : après un délai de 50ms (nécessaire pour que les custom elements comme les selects soient prêts),
prefillFields()est appelé pour remplir les champs depuis les paramètres URL. - L'attribut
data-form-loadedest ajouté pour éviter le double chargement.
Sécurité (sanitisation)
Avant l'injection dans le DOM, le HTML chargé est sanitisé par la fonction sanitizeFormHtml() :
- Les éléments
<script>,<iframe>,<object>et<embed>sont supprimés - Tous les attributs d'événements (
on*:onclick,onerror, etc.) sont retirés - Cette protection empêche les attaques XSS en cas de fichier formulaire malformé ou compromis
Chemins relatifs
Le script calcule automatiquement le chemin vers forms/ en comptant les segments de chemin après /pages/ pour déterminer le nombre de ../ nécessaires :
pages/*.html(profondeur 0) →../forms/nom.htmlpages/sous-dossier/*.html(profondeur 1) →../../forms/nom.html- Formule :
prefix = '../' × (depth + 1)
API publique
Pour intégrer un formulaire dans du contenu chargé dynamiquement (AJAX, SPA, composants), utilisez la fonction globale :
// Initialiser les embeds dans un conteneur spécifique
window.initFormEmbeds(document.querySelector('.mon-conteneur'));
Par défaut, le script initialise automatiquement tous les [data-form-embed] présents au chargement de la page. Appelez window.initFormEmbeds(container) uniquement pour du contenu injecté après le chargement initial.
Exemple complet d'intégration
<!DOCTYPE html>
<html lang="fr">
<head>
<script src="../core/js/site.js"></script>
<link rel="stylesheet" href="../core/css/tokens.css">
<link rel="stylesheet" href="../core/css/base.css">
<link rel="stylesheet" href="../core/css/forms.css">
<script src="../core/js/forms.js" defer></script>
<script src="../core/js/form-embed.js" defer></script>
<script src="../core/js/icons.js" defer></script>
</head>
<body>
<section class="section">
<div class="container">
<h2>Contactez-nous</h2>
<div data-form-embed="demande-de-devis"></div>
</div>
</section>
</body>
</html>
Anti-doublon des noms
Le constructeur protège l'unicité des noms de formulaires à trois niveaux :
- Sauvegarde : si un formulaire portant le même nom existe déjà, un suffixe numérique est ajouté automatiquement (ex :
contact-2). - Renommage : lors du renommage, le constructeur vérifie que le nouveau nom n'entre pas en conflit avec un formulaire existant.
- Duplication : le formulaire dupliqué reçoit automatiquement un nom unique (ex :
contact-copie,contact-copie-2).
Suivi d'intégration
Le constructeur offre des indicateurs visuels pour savoir quels formulaires sont intégrés dans les pages du site :
- Vue grille (badges) : dans la liste des formulaires, chaque carte affiche un badge indiquant le nombre de pages où le formulaire est intégré. Un badge vert signifie « utilisé », un badge gris signifie « non intégré ».
- Barre d'outils de l'éditeur : quand un formulaire est ouvert dans l'éditeur, un badge dans la toolbar indique s'il est intégré et dans combien de pages. Cliquer dessus affiche la liste des pages concernées.
Problèmes courants
| Problème | Cause probable | Solution |
|---|---|---|
| Le formulaire ne s'envoie pas | data-form-webhook vide ou absent |
Renseigner l'URL du webhook (ex : https://webhook.example.com/xxx) |
| La validation ne fonctionne pas | Mauvaise syntaxe data-validate |
Vérifier le format : required|email, required|min:3 |
| Un champ conditionnel ne s'affiche pas | Mauvaise valeur dans data-condition |
La valeur comparée est le slug (ex : designer-ui-ux, pas Designer UI/UX) |
| Le custom select ne se pré‑remplit pas via URL | Le formulaire est chargé via data-form-embed |
Le pré‑remplissage se fait automatiquement après le chargement (délai de 50ms) |
Erreur CORS sur /api/form.php |
SITE_ORIGIN non configuré dans .env |
Ajouter SITE_ORIGIN=https://monsite.fr dans le .env du serveur |
| Le webhook ne reçoit pas les données | Webhook URL incorrecte ou scénario inactif | Vérifier l'URL dans data-form-webhook et vérifier que le service webhook est actif |
| Le bouton reste sur « Envoi en cours... » | Le serveur webhook ne répond pas dans les 15 secondes | Le timeout restaure automatiquement le bouton — vérifier la connexion au webhook |