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=surTextField/Textarea). PourFormeSelect, le label a11y passe parariaLabel— ajoute un intitulé visible au-dessus si le contexte ne suffit pas. - Aide contextuelle. Utilise
descriptionpour 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/minpour la validation native. - Une seule action primaire. Un
Button variant="primary"(soumettre), les actions secondaires enghost. 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
labelouariaLabel). Ne laisse jamais un champ sans nom. - Ordre de tabulation = ordre visuel. La grille montant/devise ne casse pas la séquence.
- Le
Switchet laCheckboxportent leur libellé viachildren— 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.