Forme DS

Patterns & Shell

États vides & toasts

Une interface passe le plus clair de son temps dans des états intermédiaires : rien à afficher, données en cours de chargement, retour après une action. Trois patterns tokenisés couvrent l'essentiel.

Aperçu interactif

Bascule entre l'état vide (message + action de sortie), le chargement (skeleton --color-skeleton) et l'état chargé. Le bouton « Enregistrer » déclenche un toast de confirmation.

État vide

Un état vide n'est pas une erreur : c'est une invitation. Toujours message + action de sortie. Icône discrète, titre court, une phrase d'explication, puis un Button primaire qui crée la première donnée.

import { Button } from "@forme-ch/ui/controls";
import { InboxIcon, PlusIcon } from "@forme-ch/ui/icons";

<div style={{ display: "grid", gap: 10, placeItems: "center", textAlign: "center",
              color: "var(--color-text-secondary)", padding: 32 }}>
  <InboxIcon size={32} />
  <strong style={{ color: "var(--color-text-primary)" }}>Aucune facture</strong>
  <p>Crée ta première facture pour la voir apparaître ici.</p>
  <Button label="Nouvelle facture" variant="primary" icon={<PlusIcon />} onClick={create} />
</div>

Chargement — skeleton

Pendant un fetch, réserve la place avec un skeleton au lieu d'un spinner centré : la mise en page ne saute pas à l'arrivée des données. Blocs en --color-skeleton, coins arrondis, légère pulsation. Le conteneur porte aria-busy="true" et aria-live="polite".

const skel = (w) => ({
  height: 12, width: w, borderRadius: 6,
  background: "var(--color-skeleton)",
  animation: "pulse 1.4s ease-in-out infinite",
});

<div aria-busy="true" aria-live="polite" style={{ display: "grid", gap: 16 }}>
  {[0, 1, 2].map((i) => (
    <div key={i} style={{ display: "grid", gap: 8 }}>
      <div style={skel("60%")} />
      <div style={skel("35%")} />
    </div>
  ))}
</div>

// @keyframes pulse { 0%,100% { opacity: 1 } 50% { opacity: .45 } }

Fais correspondre la forme du skeleton à celle du contenu réel (nombre de lignes, largeurs) : la transition doit être imperceptible.

Toasts & retours

Après une action réussie, confirme brièvement puis efface. Un toast est transitoire (role="status", disparition auto ~2–3 s), non bloquant, avec une icône CheckCircleIcon teintée --color-success. Pour une erreur, garde le retour visible jusqu'à résolution (ne pas auto-effacer un échec).

import { CheckCircleIcon } from "@forme-ch/ui/icons";

<div role="status" style={{
  display: "inline-flex", alignItems: "center", gap: 8, padding: "8px 14px",
  borderRadius: "var(--radius-inner)", background: "var(--color-background-surface)",
  border: "1px solid var(--color-border)", boxShadow: "0 6px 20px var(--color-shadow)",
}}>
  <CheckCircleIcon size={16} color="var(--color-success)" />
  Modifications enregistrées
</div>

Accessibilité

  • Chargement : aria-busy + aria-live="polite" pour annoncer l'arrivée du contenu sans le lire caractère par caractère.
  • Toast succès : role="status" (poli). Toast erreur / alerte : role="alert" (interrompt) et pas d'auto-disparition.
  • Couleur + texte : le succès n'est jamais porté par la seule couleur verte — l'icône et le libellé le disent aussi.
  • Les skeletons sont décoratifs : ne les rends pas focusables, laisse aria-live annoncer le résultat.

Do & Don't

✓ À faire

État vide = message + action de sortie. Skeleton à la forme du contenu. Toast succès poli et transitoire ; erreur persistante en role="alert".

✕ À éviter

Une page blanche sans explication. Un spinner plein écran qui fait sauter la mise en page. Un toast d'erreur qui s'efface avant lecture. Le vert seul pour dire « ok ».