Aller au contenu
Bibliotheque UIJABS

Formulaires 20 variantes

Tout ce qui se saisit. Trois regles tenues dans chaque variante : un label lie a chaque controle (jamais un simple placeholder), une erreur qui dit comment corriger, et la contrainte annoncee avant la saisie plutot qu'apres le refus.

A. Champs de base

Texte, nombre, date, liste, curseur. Le socle .champ + .saisie s'applique a tous : changer un jeton suffit a redessiner l'ensemble.

Tel qu'il figurera sur le devis et la facture.
Champ texte

La forme complete : label lie, aide sous le champ, marque d'obligation. Le placeholder ne remplace jamais le label, il disparait a la saisie.

Voir le codeMasquer le code
<div class="champ">
  <label class="champ__label" for="f1">Nom du client <span class="requis">*</span></label>
  <input class="saisie" id="f1" type="text" placeholder="Dupont" required>
  <span class="champ__aide">Tel qu'il figurera sur le devis et la facture.</span>
</div>
/* voir le socle .champ et .saisie en haut du fichier */
flex flex-col gap-1.5 [&_input]:h-10 [&_input]:rounded-lg [&_input]:border [&_input]:px-3 focus:[&_input]:ring-2
Adresse incomplete : il manque le domaine apres le point.
Etats du champ

Les cinq etats a couvrir. Le champ valide n'est pas un simple vert : la coche confirme que la verification a eu lieu.

Voir le codeMasquer le code
<div class="demo-col demo-col--large">
  <div class="champ"><label class="champ__label">Normal</label><input class="saisie" value="Dupont"></div>
  <div class="champ"><label class="champ__label">Focus</label><input class="saisie demo-focus-champ" value="Dupont"></div>
  <div class="champ is-valide"><label class="champ__label">Valide</label>
    <span class="champ__enveloppe"><input class="saisie" value="dupont@exemple.fr"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m20 6-11 11-5-5"/></svg></span></div>
  <div class="champ is-erreur"><label class="champ__label">En erreur</label>
    <input class="saisie" value="dupont@exemple">
    <span class="champ__erreur"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 9v4M12 17h.01M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z"/></svg> Adresse incomplete : il manque le domaine apres le point.</span></div>
  <div class="champ"><label class="champ__label">Desactive</label><input class="saisie" value="Non modifiable" disabled></div>
</div>
.champ__enveloppe { position: relative; display: flex; align-items: center; }
.champ__enveloppe > svg {
  position: absolute; right: 11px;
  width: 18px; height: 18px;
  pointer-events: none;
}
.champ.is-valide .saisie   { border-color: var(--c-success); padding-right: 36px; }
.champ.is-valide svg       { color: var(--c-success); }
.champ.is-erreur .saisie   { border-color: var(--c-danger); background: var(--c-danger-soft); }
.champ.is-erreur .saisie:focus { box-shadow: 0 0 0 3px color-mix(in srgb, var(--c-danger) 22%, transparent); }
.demo-focus-champ { border-color: var(--c-primary); box-shadow: 0 0 0 3px color-mix(in srgb, var(--c-primary) 22%, transparent); }
aria-invalid:border-red-500 aria-invalid:ring-red-500/20 disabled:bg-slate-100
EUR
m2
https://
Champ avec affixe

Unite, devise, prefixe d'URL. L'affixe fait partie du cadre : il ne se selectionne pas et ne part pas dans la valeur envoyee.

Voir le codeMasquer le code
<div class="demo-col demo-col--large">
  <div class="champ"><label class="champ__label">Montant HT</label>
    <div class="saisie-groupe"><input class="saisie" type="text" value="4 280,00"><span class="saisie-groupe__affixe">EUR</span></div></div>
  <div class="champ"><label class="champ__label">Surface</label>
    <div class="saisie-groupe"><input class="saisie" type="text" value="112"><span class="saisie-groupe__affixe">m2</span></div></div>
  <div class="champ"><label class="champ__label">Site internet</label>
    <div class="saisie-groupe"><span class="saisie-groupe__affixe">https://</span><input class="saisie" type="text" value="jabsdev.fr"></div></div>
</div>
.saisie-groupe { display: flex; width: 100%; }
.saisie-groupe .saisie { border-radius: 0; }
.saisie-groupe > *:first-child { border-top-left-radius: var(--r-md); border-bottom-left-radius: var(--r-md); }
.saisie-groupe > *:last-child  { border-top-right-radius: var(--r-md); border-bottom-right-radius: var(--r-md); }
.saisie-groupe > * + * { margin-left: -1px; }
.saisie-groupe__affixe {
  display: inline-flex;
  align-items: center;
  padding: 0 var(--sp-3);
  height: var(--h-md);
  border: 1px solid var(--c-border);
  background: var(--c-surface-2);
  color: var(--c-text-muted);
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  white-space: nowrap;
  user-select: none;
}
.saisie-groupe .saisie:focus { position: relative; z-index: 1; }
flex [&_span]:bg-slate-100 [&_span]:px-3 [&_input]:rounded-l-none
Champ avec icone

Recherche, telephone, courriel. L'icone est decorative : elle ne remplace pas le label, elle accelere le reperage.

Voir le codeMasquer le code
<div class="demo-col demo-col--large">
  <div class="champ"><label class="champ__label">Rechercher</label>
    <span class="champ__enveloppe champ__enveloppe--gauche"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg><input class="saisie" type="search" placeholder="Nom, ville, reference"></span></div>
  <div class="champ"><label class="champ__label">Telephone</label>
    <span class="champ__enveloppe champ__enveloppe--gauche"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M22 16.9v3a2 2 0 0 1-2.2 2 19.8 19.8 0 0 1-8.6-3.1 19.5 19.5 0 0 1-6-6A19.8 19.8 0 0 1 2 4.2 2 2 0 0 1 4 2h3a2 2 0 0 1 2 1.7c.1 1 .4 1.9.7 2.8a2 2 0 0 1-.5 2.1L8 9.8a16 16 0 0 0 6 6l1.2-1.2a2 2 0 0 1 2.1-.5c.9.3 1.8.6 2.8.7A2 2 0 0 1 22 16.9Z"/></svg><input class="saisie" type="tel" value="06 12 34 56 78"></span></div>
</div>
.champ__enveloppe--gauche > svg { left: 11px; right: auto; color: var(--c-text-muted); }
.champ__enveloppe--gauche .saisie { padding-left: 36px; }
relative [&_svg]:absolute [&_svg]:left-3 [&_input]:pl-10
Visible par le client sur le devis.96 / 500
Zone de texte avec compteur

Message, description, commentaire. Le compteur previent AVANT la limite ; il vire au rouge quand elle est depassee, pas quand elle est atteinte.

Voir le codeMasquer le code
<div class="champ">
  <label class="champ__label" for="f5">Description des travaux</label>
  <textarea class="saisie" id="f5" rows="3">Depose de la chaudiere fioul et pose d'une pompe a chaleur air/eau 11 kW.</textarea>
  <span class="champ__compteur"><span class="champ__aide">Visible par le client sur le devis.</span><em>96 / 500</em></span>
</div>
.champ__compteur { display: flex; align-items: baseline; justify-content: space-between; gap: var(--sp-3); }
.champ__compteur em {
  font-style: normal;
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
  color: var(--c-text-muted);
  white-space: nowrap;
}
.champ__compteur em.is-proche { color: var(--c-warn); font-weight: var(--fw-semi); }
.champ__compteur em.is-depasse { color: var(--c-danger); font-weight: var(--fw-bold); }
flex justify-between [&_em]:text-xs [&_em]:tabular-nums
Liste deroulante

Plus de 6 options, ou une liste qui evoluera. En dessous de 5 options fixes, preferer des tuiles ou des radios : tout est visible d'un coup.

Voir le codeMasquer le code
<div class="demo-col demo-col--large">
  <div class="champ"><label class="champ__label" for="f6">Type d'operation</label>
    <div class="saisie-select">
      <select class="saisie" id="f6">
        <option>Pompe a chaleur air/eau</option>
        <option>Pompe a chaleur air/air</option>
        <option>Chauffe-eau thermodynamique</option>
        <option>Isolation des combles</option>
      </select><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>
    </div>
  </div>
  <div class="champ"><label class="champ__label">Departement</label>
    <div class="saisie-select"><select class="saisie" disabled><option>Selectionnez d'abord une region</option></select><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg></div></div>
</div>
.saisie-select { position: relative; display: flex; align-items: center; width: 100%; }
.saisie-select select {
  appearance: none;
  -webkit-appearance: none;
  padding-right: 34px;
  cursor: pointer;
}
.saisie-select > svg {
  position: absolute; right: 11px;
  width: 16px; height: 16px;
  color: var(--c-text-muted);
  pointer-events: none;
}
appearance-none pr-9 + chevron en absolute
Champs de date

Le selecteur natif est le bon choix : il connait la langue, le calendrier et les gestes de l'appareil. Une plage se saisit en deux champs lies.

Voir le codeMasquer le code
<div class="demo-row">
  <div class="champ" style="max-width:190px"><label class="champ__label">Date de pose</label><input class="saisie" type="date" value="2026-09-24"></div>
  <div class="champ" style="max-width:150px"><label class="champ__label">Creneau</label><input class="saisie" type="time" value="08:30"></div>
</div>
.saisie[type="date"], .saisie[type="time"], .saisie[type="datetime-local"] { padding-right: var(--sp-2); }
.saisie[type="date"]::-webkit-calendar-picker-indicator,
.saisie[type="time"]::-webkit-calendar-picker-indicator { cursor: pointer; opacity: .55; }
.saisie[type="date"]:hover::-webkit-calendar-picker-indicator { opacity: 1; }
[&::-webkit-calendar-picker-indicator]:cursor-pointer
2 000 EUR30 000 EUR
Curseur

Une valeur approximative dans une plage connue : budget, puissance, surface. La valeur exacte reste affichee, sinon le curseur est inutilisable au clavier.

Voir le codeMasquer le code
<div class="champ">
  <label class="champ__label" for="f8">Budget maximum <strong class="champ__valeur">12 000 EUR</strong></label>
  <input class="saisie-curseur" id="f8" type="range" min="2000" max="30000" step="500" value="12000">
  <span class="champ__bornes"><span>2 000 EUR</span><span>30 000 EUR</span></span>
</div>
.champ__valeur { float: right; font-weight: var(--fw-bold); color: var(--c-primary-ink); }
.champ__bornes { display: flex; justify-content: space-between; font-size: var(--fs-xs); color: var(--c-text-muted); }
.saisie-curseur {
  appearance: none;
  -webkit-appearance: none;
  width: 100%;
  height: 6px;
  margin: var(--sp-3) 0;
  border-radius: var(--r-full);
  background: var(--c-surface-3);
  cursor: pointer;
}
.saisie-curseur::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 22px; height: 22px;
  border-radius: 50%;
  background: var(--c-primary);
  border: 3px solid var(--c-surface);
  box-shadow: var(--sh-2);
  cursor: grab;
}
.saisie-curseur::-moz-range-thumb {
  width: 22px; height: 22px;
  border-radius: 50%;
  background: var(--c-primary);
  border: 3px solid var(--c-surface);
  box-shadow: var(--sh-2);
  cursor: grab;
}
.saisie-curseur:focus-visible { outline: 2px solid var(--c-focus); outline-offset: 4px; }
appearance-none h-1.5 rounded-full bg-slate-200 [&::-webkit-slider-thumb]:h-5 [&::-webkit-slider-thumb]:w-5

B. Choix et saisies composees

Suggestions, selection multiple, mot de passe, code a usage unique, montant. Les cas ou un simple input ne suffit pas.

  • 12 rue des Lilas93330 Neuilly-sur-Marne
  • 12 rue des Lilas92700 Colombes
  • 12 bis rue des Lilas94100 Saint-Maur-des-Fosses
Saisie avec suggestions

Adresse, ville, client existant. La suggestion survolee est mise en avant, et la saisie libre reste possible : ne jamais bloquer sur une liste incomplete.

Voir le codeMasquer le code
<div class="champ champ--suggestions">
  <label class="champ__label">Adresse du chantier</label>
  <span class="champ__enveloppe champ__enveloppe--gauche"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg><input class="saisie" value="12 rue des Lil"></span>
  <ul class="suggestions">
    <li class="is-actif"><strong>12 rue des Lilas</strong><small>93330 Neuilly-sur-Marne</small></li>
    <li><strong>12 rue des Lilas</strong><small>92700 Colombes</small></li>
    <li><strong>12 bis rue des Lilas</strong><small>94100 Saint-Maur-des-Fosses</small></li>
  </ul>
</div>
.champ--suggestions { position: relative; }
.suggestions {
  list-style: none;
  margin: 4px 0 0;
  padding: 4px;
  width: 100%;
  border: 1px solid var(--c-border);
  border-radius: var(--r-md);
  background: var(--c-surface);
  box-shadow: var(--sh-3);
  z-index: 10;
}
.suggestions li {
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r-sm);
  cursor: pointer;
}
.suggestions li:hover, .suggestions li.is-actif { background: var(--c-primary-soft); }
.suggestions strong { font-size: var(--fs-md); font-weight: var(--fw-semi); color: var(--c-text); }
.suggestions small  { font-size: var(--fs-xs); color: var(--c-text-muted); }
absolute z-10 mt-1 w-full rounded-lg border bg-white shadow-lg [&_li]:px-3 [&_li]:py-2
Pompe a chaleur Isolation combles
Tapez pour rechercher, Entree pour ajouter.
Selection multiple

Etiqueter un dossier, choisir des competences. Les valeurs retenues restent dans le champ, chacune retirable individuellement.

Voir le codeMasquer le code
<div class="champ">
  <label class="champ__label">Operations du dossier</label>
  <div class="saisie saisie--jetons">
    <span class="btn-etiquette btn-etiquette--accent">Pompe a chaleur <button aria-label="Retirer"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button></span>
    <span class="btn-etiquette btn-etiquette--accent">Isolation combles <button aria-label="Retirer"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button></span>
    <input type="text" placeholder="Ajouter...">
  </div>
  <span class="champ__aide">Tapez pour rechercher, Entree pour ajouter.</span>
</div>
.saisie--jetons {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  height: auto;
  min-height: var(--h-md);
  padding: 6px;
}
.saisie--jetons:focus-within { border-color: var(--c-primary); box-shadow: 0 0 0 3px color-mix(in srgb, var(--c-primary) 22%, transparent); }
.saisie--jetons input {
  flex: 1;
  min-width: 100px;
  border: none;
  background: transparent;
  color: var(--c-text);
  font-family: var(--font-ui);
  font-size: var(--fs-md);
  outline: none;
}
flex h-auto flex-wrap items-center gap-1.5 p-2 [&_input]:flex-1 [&_input]:border-0 [&_input]:outline-none
Bon mot de passe. 12 caracteres minimum, une majuscule et un chiffre.
Mot de passe

L'oeil qui devoile est obligatoire : la saisie a l'aveugle sur mobile est la premiere cause d'echec de connexion. La jauge evalue, elle ne bloque pas.

Voir le codeMasquer le code
<div class="champ">
  <label class="champ__label" for="fb3">Mot de passe</label>
  <span class="champ__enveloppe">
    <input class="saisie" id="fb3" type="text" value="Chantier2026!">
    <button class="btn btn--icone btn--fantome btn--sm champ__oeil" type="button" aria-label="Masquer le mot de passe" aria-pressed="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg></button>
  </span>
  <span class="jauge-mdp" data-niveau="3"><i></i><i></i><i></i><i></i></span>
  <span class="champ__aide">Bon mot de passe. 12 caracteres minimum, une majuscule et un chiffre.</span>
</div>
.champ__oeil { position: absolute; right: 4px; color: var(--c-text-muted); }
.champ__enveloppe:has(.champ__oeil) .saisie { padding-right: 40px; }
.jauge-mdp { display: flex; gap: 4px; margin-top: 2px; }
.jauge-mdp i { height: 4px; flex: 1; border-radius: var(--r-full); background: var(--c-surface-3); }
.jauge-mdp[data-niveau="1"] i:nth-child(-n+1) { background: var(--c-danger); }
.jauge-mdp[data-niveau="2"] i:nth-child(-n+2) { background: var(--c-warn); }
.jauge-mdp[data-niveau="3"] i:nth-child(-n+3) { background: var(--c-success); }
.jauge-mdp[data-niveau="4"] i                 { background: var(--c-success); }
[&_i]:h-1 [&_i]:flex-1 [&_i]:rounded-full [&_i]:bg-slate-200
Envoye au 06 12 34 56 78. Renvoyer dans 24 s
Code a usage unique

Verification par SMS ou courriel. Un champ par chiffre, le collage repartit le code automatiquement, et inputmode ouvre le pave numerique.

Voir le codeMasquer le code
<div class="champ">
  <label class="champ__label">Code recu par SMS</label>
  <div class="saisie-code">
    <input class="saisie" inputmode="numeric" maxlength="1" value="4" aria-label="Chiffre 1">
    <input class="saisie" inputmode="numeric" maxlength="1" value="8" aria-label="Chiffre 2">
    <input class="saisie" inputmode="numeric" maxlength="1" value="1" aria-label="Chiffre 3">
    <input class="saisie" inputmode="numeric" maxlength="1" value="" aria-label="Chiffre 4">
    <input class="saisie" inputmode="numeric" maxlength="1" value="" aria-label="Chiffre 5">
    <input class="saisie" inputmode="numeric" maxlength="1" value="" aria-label="Chiffre 6">
  </div>
  <span class="champ__aide">Envoye au 06 12 34 56 78. <a href="#">Renvoyer dans 24 s</a></span>
</div>
.saisie-code { display: flex; gap: var(--sp-2); }
.saisie-code .saisie {
  width: 46px;
  height: 54px;
  padding: 0;
  text-align: center;
  font-size: 21px;
  font-weight: var(--fw-bold);
  font-variant-numeric: tabular-nums;
}
.saisie-code .saisie:not(:placeholder-shown) { border-color: var(--c-primary); }
flex gap-2 [&_input]:h-12 [&_input]:w-11 [&_input]:text-center [&_input]:text-lg [&_input]:font-bold
EUR
EUR
Champ monetaire

Montants d'un devis. Chiffres tabulaires pour que les colonnes s'alignent, alignement a droite, et le calcul du TTC affiche a cote plutot que devine.

Voir le codeMasquer le code
<div class="demo-row">
  <div class="champ" style="max-width:200px"><label class="champ__label">Montant HT</label>
    <div class="saisie-groupe"><input class="saisie saisie--nombre" value="4 000,00"><span class="saisie-groupe__affixe">EUR</span></div></div>
  <div class="champ" style="max-width:130px"><label class="champ__label">TVA</label>
    <div class="saisie-select"><select class="saisie"><option>5,5 %</option><option>10 %</option><option>20 %</option></select><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg></div></div>
  <div class="champ" style="max-width:200px"><label class="champ__label">Montant TTC</label>
    <div class="saisie-groupe"><input class="saisie saisie--nombre" value="4 220,00" readonly><span class="saisie-groupe__affixe">EUR</span></div></div>
</div>
.saisie--nombre { text-align: right; font-variant-numeric: tabular-nums; font-weight: var(--fw-semi); }
text-right tabular-nums

C. Mise en page

Grilles, sections, barre d'enregistrement, assistant par etapes, recapitulatif d'erreurs, formulaire de connexion.

Grille de formulaire

Les champs courts se partagent une ligne, les champs longs occupent toute la largeur. Sous 640 px, tout repasse sur une colonne.

Voir le codeMasquer le code
<div class="form-grille">
  <div class="champ col-6"><label class="champ__label">Prenom</label><input class="saisie" value="Julien"></div>
  <div class="champ col-6"><label class="champ__label">Nom</label><input class="saisie" value="Krissi"></div>
  <div class="champ col-12"><label class="champ__label">Adresse</label><input class="saisie" value="12 rue des Lilas"></div>
  <div class="champ col-4"><label class="champ__label">Code postal</label><input class="saisie" value="93330"></div>
  <div class="champ col-8"><label class="champ__label">Ville</label><input class="saisie" value="Neuilly-sur-Marne"></div>
</div>
.form-grille {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: var(--sp-4);
  width: 100%;
  /* Voir la note de .dash : la grille suit son conteneur, pas la fenetre. */
  container-type: inline-size;
}
.col-2 { grid-column: span 2; } .col-3 { grid-column: span 3; } .col-4 { grid-column: span 4; }
.col-6 { grid-column: span 6; } .col-8 { grid-column: span 8; } .col-9 { grid-column: span 9; }
.col-12 { grid-column: span 12; }
@container (max-width: 640px) { .form-grille > * { grid-column: span 12; } }
@supports not (container-type: inline-size) {
  @media (max-width: 640px) { .form-grille > * { grid-column: span 12; } }
}
grid grid-cols-12 gap-4 [&_.col-6]:col-span-6 max-sm:[&>*]:col-span-12
Coordonnees du client

Ces informations figureront sur le devis.

Chantier

L'adresse d'intervention si elle differe.

Sections de formulaire

Au-dela de 8 champs, decouper. Chaque section porte un titre et une phrase qui dit a quoi elle sert : l'utilisateur sait ou il en est.

Voir le codeMasquer le code
<form class="form-sections">
  <fieldset>
    <legend>Coordonnees du client</legend>
    <p class="form-sections__note">Ces informations figureront sur le devis.</p>
    <div class="form-grille">
      <div class="champ col-6"><label class="champ__label">Nom</label><input class="saisie" value="Krissi"></div>
      <div class="champ col-6"><label class="champ__label">Telephone</label><input class="saisie" value="06 12 34 56 78"></div>
    </div>
  </fieldset>
  <fieldset>
    <legend>Chantier</legend>
    <p class="form-sections__note">L'adresse d'intervention si elle differe.</p>
    <div class="form-grille">
      <div class="champ col-12"><label class="champ__label">Adresse</label><input class="saisie" placeholder="Identique aux coordonnees"></div>
    </div>
  </fieldset>
</form>
.form-sections { display: flex; flex-direction: column; gap: var(--sp-6); width: 100%; }
.form-sections fieldset { margin: 0; padding: var(--sp-5) 0 0; border: none; border-top: 1px solid var(--c-border); }
.form-sections fieldset:first-child { border-top: none; padding-top: 0; }
.form-sections legend { padding: 0; font-size: var(--fs-lg); font-weight: var(--fw-bold); color: var(--c-text); }
.form-sections__note { margin: 4px 0 var(--sp-4); font-size: var(--fs-sm); color: var(--c-text-muted); }
[&_fieldset]:border-t [&_fieldset]:pt-5 [&_legend]:text-base [&_legend]:font-bold
Raison socialeNom juridique de l'entreprise
Numero SIRET14 chiffres, sans espace
Formulaire en deux colonnes

Ecrans larges d'application interne : l'intitule a gauche, le controle a droite. Sous 760 px, l'intitule repasse au-dessus.

Voir le codeMasquer le code
<div class="form-deux">
  <div class="form-deux__ligne">
    <div class="form-deux__tete"><strong>Raison sociale</strong><small>Nom juridique de l'entreprise</small></div>
    <input class="saisie" value="JABS">
  </div>
  <div class="form-deux__ligne">
    <div class="form-deux__tete"><strong>Numero SIRET</strong><small>14 chiffres, sans espace</small></div>
    <input class="saisie" value="90212345600014">
  </div>
</div>
.form-deux { display: flex; flex-direction: column; gap: var(--sp-5); width: 100%; }
.form-deux__ligne { display: grid; grid-template-columns: 220px 1fr; gap: var(--sp-6); align-items: start; }
.form-deux__tete { display: flex; flex-direction: column; gap: 2px; }
.form-deux__tete strong { font-size: var(--fs-md); font-weight: var(--fw-semi); }
.form-deux__tete small  { font-size: var(--fs-xs); color: var(--c-text-muted); line-height: 1.45; }
@media (max-width: 760px) { .form-deux__ligne { grid-template-columns: 1fr; gap: var(--sp-2); } }
grid grid-cols-[240px_1fr] gap-x-8 gap-y-6 max-md:grid-cols-1
Modifications non enregistrees
Barre d'enregistrement

Collee en bas du formulaire, elle n'apparait qu'une fois une modification faite. Elle rappelle qu'il reste des changements non enregistres.

Voir le codeMasquer le code
<div class="form-barre">
  <span class="form-barre__note"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 9v4M12 17h.01M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z"/></svg> Modifications non enregistrees</span>
  <span class="cat-head__spacer"></span>
  <button class="btn btn--secondaire">Annuler</button>
  <button class="btn btn--plein">Enregistrer</button>
</div>
.form-barre {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  width: 100%;
  padding: var(--sp-3);
  border: 1px solid var(--c-border);
  border-radius: var(--r-md);
  background: color-mix(in srgb, var(--c-surface) 92%, transparent);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  box-shadow: var(--sh-2);
}
.form-barre__note { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fs-sm); font-weight: var(--fw-semi); color: var(--c-warn); }
.form-barre__note svg { width: 15px; height: 15px; }
@media (max-width: 560px) { .form-barre { flex-wrap: wrap; } .form-barre .btn { flex: 1; } }
sticky bottom-0 flex items-center gap-3 border-t bg-white/90 p-3 backdrop-blur
Assistant par etapes

Formulaire long decoupe en etapes. Les etapes franchies restent cliquables pour revenir en arriere, sinon la correction oblige a tout refaire.

Voir le codeMasquer le code
<nav class="form-etapes" aria-label="Progression">
  <a href="#" class="is-fait"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m20 6-11 11-5-5"/></svg><span>Client</span></a>
  <a href="#" class="is-fait"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m20 6-11 11-5-5"/></svg><span>Chantier</span></a>
  <a href="#" class="is-courante" aria-current="step"><em>3</em><span>Operations</span></a>
  <span><em>4</em><span>Recapitulatif</span></span>
</nav>
.form-etapes { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); width: 100%; }
.form-etapes > * {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r-full);
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  color: var(--c-text-muted);
  text-decoration: none;
}
.form-etapes em, .form-etapes svg {
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px;
  flex: none;
  border-radius: var(--r-full);
  background: var(--c-surface-3);
  font-style: normal;
  font-size: 11px;
  font-weight: var(--fw-bold);
}
.form-etapes svg { padding: 4px; }
.form-etapes .is-fait { color: var(--c-success); }
.form-etapes .is-fait svg { background: var(--c-success); color: #fff; }
.form-etapes .is-fait:hover { background: var(--c-success-soft); }
.form-etapes .is-courante { background: var(--c-primary-soft); color: var(--c-primary-ink); font-weight: var(--fw-bold); }
.form-etapes .is-courante em { background: var(--c-primary); color: var(--c-primary-text); }
flex items-center gap-2 [&_a]:flex [&_a]:items-center [&_a]:gap-2 aria-current:font-bold
Recapitulatif d'erreurs

En tete du formulaire apres un envoi refuse. Chaque erreur est un lien qui amene au champ : sur un formulaire long, chercher soi-meme est decourageant.

Voir le codeMasquer le code
<div class="form-erreurs" role="alert">
  <strong><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 9v4M12 17h.01M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z"/></svg> 3 champs doivent etre corriges</strong>
  <ul>
    <li><a href="#">Adresse de courriel</a> : il manque le domaine apres le point.</li>
    <li><a href="#">Code postal</a> : 5 chiffres attendus.</li>
    <li><a href="#">Date de pose</a> : la date doit etre posterieure a aujourd'hui.</li>
  </ul>
</div>
.form-erreurs {
  width: 100%;
  padding: var(--sp-4);
  border: 1px solid var(--c-danger);
  border-radius: var(--r-md);
  background: var(--c-danger-soft);
}
.form-erreurs strong { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-md); color: var(--c-danger); }
.form-erreurs strong svg { width: 17px; height: 17px; }
.form-erreurs ul { margin: var(--sp-2) 0 0; padding-left: var(--sp-6); font-size: var(--fs-sm); color: var(--c-text); }
.form-erreurs li { margin-top: 4px; line-height: 1.5; }
.form-erreurs a { color: var(--c-danger); font-weight: var(--fw-semi); }
rounded-lg border border-red-300 bg-red-50 p-4 [&_a]:font-semibold [&_a]:underline

Connexion

ou
Mot de passe oublie
Formulaire de connexion

L'assemblage complet : connexion externe en premier, separateur, puis la voie classique. L'ordre inverse fait rater le bouton Google a la moitie des gens.

Voir le codeMasquer le code
<form class="form-carte">
  <h3>Connexion</h3>
  <button class="btn btn--social btn--bloc" type="button"><svg viewBox="0 0 24 24" aria-hidden="true"><path fill="#4285F4" d="M23 12.3c0-.8-.1-1.6-.2-2.3H12v4.5h6.2a5.3 5.3 0 0 1-2.3 3.5v2.9h3.7c2.2-2 3.4-5 3.4-8.6Z"/><path fill="#34A853" d="M12 23.5c3.1 0 5.7-1 7.6-2.8l-3.7-2.9c-1 .7-2.3 1.1-3.9 1.1-3 0-5.5-2-6.4-4.7H1.8v3A11.5 11.5 0 0 0 12 23.5Z"/><path fill="#FBBC05" d="M5.6 14.2a6.9 6.9 0 0 1 0-4.4v-3H1.8a11.5 11.5 0 0 0 0 10.4Z"/><path fill="#EA4335" d="M12 5.1c1.7 0 3.2.6 4.4 1.7l3.3-3.3A11.5 11.5 0 0 0 1.8 6.8l3.8 3a6.9 6.9 0 0 1 6.4-4.7Z"/></svg> Continuer avec Google</button>
  <div class="form-carte__ou"><span>ou</span></div>
  <div class="champ"><label class="champ__label" for="fc1">Adresse de courriel</label><input class="saisie" id="fc1" type="email" autocomplete="username"></div>
  <div class="champ"><label class="champ__label" for="fc2">Mot de passe</label><input class="saisie" id="fc2" type="password" autocomplete="current-password"></div>
  <div class="form-carte__ligne">
    <label class="coche"><input type="checkbox" checked><span class="coche__boite"></span> Rester connecte</label>
    <a href="#" class="btn btn--lien">Mot de passe oublie</a>
  </div>
  <button class="btn btn--plein btn--bloc btn--lg">Se connecter</button>
</form>
.form-carte {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  width: 100%;
  max-width: 380px;
  padding: var(--sp-6);
  border: 1px solid var(--c-border);
  border-radius: var(--r-lg);
  background: var(--c-surface);
  box-shadow: var(--sh-1);
}
.form-carte h3 { margin: 0; font-size: 20px; font-weight: var(--fw-bold); }
.form-carte__ou { display: flex; align-items: center; gap: var(--sp-3); color: var(--c-text-muted); font-size: var(--fs-xs); }
.form-carte__ou::before, .form-carte__ou::after { content: ""; flex: 1; height: 1px; background: var(--c-border); }
.form-carte__ligne { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); font-size: var(--fs-sm); }
mx-auto flex w-full max-w-sm flex-col gap-4 rounded-xl border bg-white p-6 shadow-sm