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>
AttributDescription
data-form-webhookURL(s) du webhook (POST JSON) — une ou plusieurs URLs séparées par des virgules
data-form-redirectURL de redirection après succès
data-form-successMessage toast de succès
data-form-errorMessage 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>
ClasseDescription
form__field--floatActive le label flottant sur le champ
form__label-hintTexte d'indication sous le label (ex : « Optionnel », « Si vous avez déjà un site »)
form__errorConteneur 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-shown pour 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__error est 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__next passe à l'étape suivante (avec validation des champs de l'étape courante)
  • .form__prev revient à l'étape précédente
  • Les champs required des é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=NomDuStep permet de démarrer directement sur une étape spécifique (la valeur correspond au data-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 des data-step-label de chaque step. L'indicateur actif reçoit la classe form__step-indicator--active, les étapes passées reçoivent form__step-indicator--done.
  • .form__progress : conteneur de la barre de progression. Contient .form__progress-bar dont 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

AttributDescription
data-step-labelLabel 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 required des étapes non actives sont automatiquement désactivés
  • Transitions animées (fade + translate) entre les étapes
  • Paramètre URL ?step=NomDuStep pour 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ègleDescription
requiredChamp obligatoire
emailFormat email valide
phoneFormat téléphone valide
urlFormat URL (http/https)
min:NMinimum N caractères
max:NMaximum N caractères
regex:/pattern/Regex personnalisé
regex:/pattern/:MessageRegex 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__error s'il existe, sinon il est créé automatiquement. Le wrapper .form__group ou .form__field le plus proche sert de conteneur pour l'état d'erreur. Les messages d'erreur possèdent role="alert" et aria-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 :

PatternUsage
^[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 :

Particulier
Professionnel
<!-- 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

FormatDescriptionExemple
champ=valeurÉgalité strictetype=pro
champ!=valeurDifférent detype!=perso
champ=val1,val2L’une des valeurs (OR implicite)poste=designer-ui-ux,fullstack
champ=*Non vide (le champ a une valeur)email=*
champ>NSupérieur à (comparaison numérique)budget>5000
champ<NInférieur à (comparaison numérique)budget<10000

Combinaisons (AND / OR)

SyntaxeDescription
cond1&&cond2AND : toutes les conditions doivent être vraies
cond1||cond2OR : 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 name ou data-name du 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

Choisir un pays...
France
Belgique
Suisse
<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

S
M
L
<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 :

S
M
L
Autre :
<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

Option 1
Option 2
Option 3
<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 :

Option 1
Option 2
Autre :
<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 :

#6366F1
<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 :

050100
<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 (grabgrabbing)
  • 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.

Glissez un fichier ici ou parcourir image/*,.pdf
<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>
AttributDescription
acceptTypes de fichiers acceptés (ex : image/*, .pdf,.doc,.docx)
multipleAutorise 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.php forward 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>
AttributDescription
data-formulaExpression mathématique avec {nom_champ} comme variables
data-decimalsNombre 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() ni new Function(). Seuls les nombres et les opérateurs +, -, *, /, () sont acceptés — toute expression contenant d'autres caractères retourne NaN. 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 :

Design
Développement
SEO
<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 :

AttributColonnesLargeurClasse CSS
data-field-width="1-1" (défaut)span 6100%(aucune)
data-field-width="1-2"span 350%form__group--half
data-field-width="1-3"span 233.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 :

ClasseBreakpointLargeur
form__group--tablet-full≤ 991px100%
form__group--tablet-half≤ 991px50%
form__group--tablet-third≤ 991px33.33%
form__group--mobile-full≤ 767px100%
form__group--mobile-half≤ 767px50%
form__group--mobile-third≤ 767px33.33%
form__group--sm-full≤ 478px100%
form__group--sm-half≤ 478px50%
form__group--sm-third≤ 478px33.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> :

AttributDescriptionValeurs
data-form-gapGap entre les champs (grille)xs, sm, md, base, lg, xl, 2xl, 3xl
data-btn-gapGap entre les boutons d'actionxs, sm, md, base, lg, xl, 2xl, 3xl
data-btn-marginMarge au-dessus du bloc boutonsxs, 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 :

WebhookRedirectComportement
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

Choisir...
HTML
CSS
JavaScript
<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 page
  • user_agent : navigateur de l'utilisateur
  • utm_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ètreTypeDéfautDescription
messagestringTexte affiché
typestring'info'success, error, warning, info
durationnumber4000Duré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.

Problèmes courants
  • Le formulaire ne s'envoie pas : vérifiez que l'attribut data-form-webhook contient une URL de webhook valide (ex : https://webhook.example.com/xxx).
  • La validation ne fonctionne pas : ajoutez l'attribut data-validate sur 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 portant data-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'attribut name (pas l'id).
  • Le toast ne s'affiche pas : appelez showToast('message', 'success') depuis la console pour tester. Vérifiez que forms.css est 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.

Liste des formulaires dans le constructeur

Interface

L'interface est divisée en trois zones :

ZonePositionContenu
ÉtapesGaucheListe des steps avec drag & drop pour réordonner
Éditeur visuelCentreAperçu WYSIWYG du formulaire, glisser-déposer des champs
Panneau latéralDroitePropriétés du champ sélectionné, paramètres, aperçu live, code généré
Éditeur visuel de formulaire — étapes et champs

Cliquez sur un champ pour afficher son panneau de configuration à droite : label, name, placeholder, validation, design et logique conditionnelle.

Configuration d'un champ — label, name, placeholder, validation

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 (&#9881;) dans le panneau latéral permet de configurer :

  • Webhook URL : endpoint pour l'envoi des données (/api/form.php recommandé)
  • 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 / .htm sont 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.

CasRéponseMessage
Fichier > 1 Mo413Fichier trop volumineux — Taille reçue X octets, maximum autorisé 1048576 octets (1024 Ko)
HTML sans form__step ni form__group400Aucune 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 champ400Formulaire vide — Le formulaire ne contient aucun champ détecté.
Plus de 100 étapes400Trop d'étapes — N étapes détectées, maximum 100.
Plus de 1000 champs400Trop de champs — N champs détectés, maximum 1000.
HTML invalide / non parsable400HTML 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 :

  1. Détection : le script recherche tous les éléments [data-form-embed] dans la page.
  2. Chargement : pour chaque conteneur, il charge le fichier HTML correspondant depuis forms/{nom}.html via fetch(), avec un timeout de 8 secondes. Si le chargement échoue ou dépasse le timeout, un message d'erreur s'affiche dans le conteneur.
  3. Injection : le HTML est injecté dans le conteneur data-form-embed.
  4. 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.
  5. 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.
  6. L'attribut data-form-loaded est 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.html
  • pages/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

Voir aussi