Forme DS

Patterns & Shell

Formulaires

Assembler un formulaire réel avec les contrôles du DS : un label par champ, des groupes logiques, une aide contextuelle et une seule action primaire. Chaque contrôle est auto-stylé par Astryx — l'aperçu ci-dessous est pleinement interactif.

Aperçu — création de facture

FormeSelect, TextField (dont un champ isNumeric aligné à droite), FormeDatePicker, Textarea, Switch, Checkbox et deux Button. Le bouton primaire reste désactivé tant que les CGV ne sont pas cochées.

import {
  TextField, Textarea, FormeSelect, Checkbox, Switch, FormeDatePicker, Button,
} from "@forme-ch/ui/controls";

<form onSubmit={handleSubmit} style={{ display: "grid", gap: 18, maxWidth: 460 }}>
  <FormeSelect ariaLabel="Client" placeholder="Choisir un client"
    value={client} onChange={setClient} options={CLIENTS} isRequired />

  <TextField label="Référence" isLabelHidden={false}
    value={ref} onChange={setRef} placeholder="ex. FAC-2025-0192"
    description="Identifiant unique visible sur le document." isRequired />

  <div style={{ display: "grid", gridTemplateColumns: "1fr 120px", gap: 12 }}>
    <TextField label="Montant" isLabelHidden={false} align="right" isNumeric
      inputMode="decimal" step="0.05" min={0} value={montant} onChange={setMontant} />
    <FormeSelect ariaLabel="Devise" value={devise} onChange={setDevise} options={DEVISES} />
  </div>

  <FormeDatePicker label="Échéance" value={echeance} onChange={setEcheance} isRequired />
  <Textarea label="Notes internes" isLabelHidden={false} rows={3} maxLength={500}
    value={notes} onChange={setNotes} />

  <Switch isSelected={recurrente} onChange={setRecurrente}>Facture récurrente (mensuelle)</Switch>
  <Checkbox isSelected={cgv} onChange={setCgv} description="Obligatoire avant l'envoi.">
    J'atteste que les conditions générales ont été acceptées
  </Checkbox>

  <div style={{ display: "flex", gap: 10, justifyContent: "flex-end" }}>
    <Button label="Annuler" variant="ghost" type="reset" />
    <Button label="Créer la facture" variant="primary" type="submit" isDisabled={!cgv} />
  </div>
</form>

Bonnes pratiques

  • Un label par champ. Rends-le visible (isLabelHidden= sur TextField/Textarea). Pour FormeSelect, le label a11y passe par ariaLabel — ajoute un intitulé visible au-dessus si le contexte ne suffit pas.
  • Aide contextuelle. Utilise description pour expliquer le format attendu, pas un placeholder — le placeholder disparaît à la saisie.
  • Groupement. Rapproche les champs liés (montant + devise) dans une même ligne en grille. Garde un rythme vertical régulier.
  • Champs numériques. align="right" + isNumeric + inputMode="decimal" pour les montants ; step/min pour la validation native.
  • Une seule action primaire. Un Button variant="primary" (soumettre), les actions secondaires en ghost. Aligne les actions à droite.
  • Requis. Marque les champs obligatoires avec isRequired ; verrouille la soumission tant que les conditions ne sont pas remplies.

Server Actions (RSC)

En mode non-contrôlé, donne un name à chaque contrôle : la valeur est portée dans le <form> natif (via un input masqué pour FormeSelect, FormeDatePicker, Segmented, FileInput). Tu peux alors lire le FormData côté Server Action sans état client.

Accessibilité

  • Chaque contrôle expose un nom accessible (le label ou ariaLabel). Ne laisse jamais un champ sans nom.
  • Ordre de tabulation = ordre visuel. La grille montant/devise ne casse pas la séquence.
  • Le Switch et la Checkbox portent leur libellé via children — la cible de clic couvre le texte.

Do & Don't

✓ À faire

Un label par champ, l'aide dans description, une action primaire unique alignée à droite, requis explicites.

✕ À éviter

Remplacer le label par un placeholder. Deux boutons primary. Enchaîner 15 champs sans regroupement. Mélanger contrôles DS et <input> bruts.