← Blog

UX d’un sélecteur de variantes en React : tailles épuisées, combinaisons manquantes et retour en stock

Couleurs et tailles semblent simples jusqu’à ce que le stock dépende de chaque combinaison. Un sélecteur de variantes React pour les tailles épuisées, les combinaisons manquantes, les photos, les prix et l’URL.

Preet Suthar · · 5 min de lecture

  • React
  • E-commerce
  • UX
  • Accessibility

Pour votre entrepôt, un t-shirt en trois couleurs et cinq tailles, ce sont quinze produits. Le stock est porté par chaque combinaison, pas par « Oat » ou « Large ». La plupart des sélecteurs de variantes l’ignorent. Ils affichent les couleurs et les tailles comme deux listes indépendantes, et l’acheteur découvre que Oat n’existe pas en XL quand Ajouter au panier échoue.

Voici comment construire en React un sélecteur de variantes qui dit la vérité sur le stock : tailles épuisées, combinaisons qui n’existent pas, e-mails de retour en stock, et photos et prix qui suivent le choix.

Modéliser les variantes, en déduire les options

Partez des variantes, parce que c’est ce que renvoie votre API e-commerce et ce vers quoi pointe une commande :

type Variant = {
  id: string
  options: Record<string, string>
  available: boolean
  quantity?: number
  price: Money
  imageId?: string
}

type Product = {
  options: { name: string; values: string[] }[]
  variants: Variant[]
}

Chaque valeur à l’écran reçoit l’un de trois états, calculé à partir des variantes et du reste de la sélection en cours :

  • Disponible : une variante avec cette valeur et les autres options choisies a du stock.
  • Épuisé : la combinaison existe mais n’a pas de stock en ce moment.
  • Indisponible : la combinaison n’est pas fabriquée du tout.
type ValueState = "available" | "sold-out" | "unavailable"

function getValueState(
  product: Product,
  selection: Record<string, string>,
  name: string,
  value: string
): ValueState {
  const wanted = { ...selection, [name]: value }
  const matches = product.variants.filter((variant) =>
    Object.entries(wanted).every(
      ([key, chosen]) => variant.options[key] === chosen
    )
  )
  if (matches.length === 0) return "unavailable"
  return matches.some((variant) => variant.available) ? "available" : "sold-out"
}

Vous obtenez ainsi le comportement attendu par les acheteurs, sans cas particuliers. Une couleur reste disponible tant qu’une de ses tailles est en stock. Une taille n’est barrée que pour les couleurs dans lesquelles elle est épuisée. Choisissez une autre couleur et les tailles se mettent à jour.

C’est une fonction pure, elle tourne donc aussi sur le serveur. Faites le premier affichage avec les bons états au lieu de montrer toutes les tailles comme disponibles pour en barrer certaines ensuite.

Des pastilles et des boutons, jamais une liste déroulante

Un <select> cache la seule chose que les gens sont venus vérifier : si leur taille est en stock. Affichez les couleurs sous forme de pastilles et les tailles sous forme de boutons, pour que tout soit visible d’un coup d’œil.

Sémantiquement, chaque option est un groupe de boutons radio. Utilisez de vrais inputs radio, ou role="radiogroup" avec un focus itinérant, pour que les flèches du clavier passent d’une valeur à l’autre et que le groupe ait un nom. Exposez aussi l’état aux technologies d’assistance, parce qu’un trait barré est invisible pour un lecteur d’écran et facile à manquer pour tous les autres :

<label className="swatch" data-state={state}>
  <input
    type="radio"
    name="size"
    value={size}
    checked={selection.size === size}
    onChange={() => select("size", size)}
    disabled={state === "unavailable"}
    aria-describedby={state === "sold-out" ? "size-sold-out" : undefined}
  />
  {size}
</label>

Au-dessus des pastilles, affichez la valeur choisie en texte (« Couleur : Oat »). Au survol, prévisualisez-y le nom de la valeur survolée, pour que les gens n’aient pas à deviner une couleur à partir d’un cercle de 32px.

Une valeur épuisée doit rester sélectionnable

L’erreur classique consiste à désactiver les tailles épuisées. Cela cache l’information la plus utile, à savoir quelles tailles existent, et supprime le chemin vers un e-mail de retour en stock.

Gardez les valeurs épuisées sélectionnables. Quand la variante choisie est épuisée, remplacez Ajouter au panier par un formulaire « M’avertir du retour en stock » qui enregistre l’id de la variante, pas celui du produit, pour que l’e-mail parle de Oat en Large et pas de « ce t-shirt ».

Seules les combinaisons qui n’existent pas doivent être désactivées, et elles doivent dire pourquoi dans leur nom accessible : « XL, non fabriqué en Oat ».

Ne présélectionnez pas la taille

Présélectionner la première taille, c’est comme ça que les boutiques expédient beaucoup de S. Présélectionnez la couleur, puisque c’est ce que la photo montre déjà, et laissez la taille vide.

Quand quelqu’un appuie sur Ajouter au panier sans taille, n’échouez pas en silence et n’ouvrez pas d’alerte. Marquez le groupe de tailles avec « Sélectionnez une taille », déplacez-y le focus et secouez-le brièvement, une secousse que le mouvement réduit désactive.

function addToBag() {
  const missing = product.options.find((option) => !selection[option.name])
  if (missing) {
    setError(missing.name, `Select a ${missing.name.toLowerCase()}`)
    groupRefs.current[missing.name]?.focus()
    return
  }
  add(findVariant(product, selection))
}

Passer à une combinaison qui n’existe pas

L’acheteur a sélectionné Large, puis choisit une couleur qui n’existe qu’en Small et Medium. Vous avez deux options : bloquer la couleur, ou l’accepter et vider la taille. Vider la taille est préférable, parce que la couleur est généralement la décision la plus importante. Mais dites-le. Une live region polie annonçant « Large n’existe pas en Rust. Sélectionnez une taille. » évite aux utilisateurs de lecteurs d’écran d’ajouter le mauvais article, et le groupe de tailles visible montre la sélection vide à tous les autres.

Les photos et le prix suivent le choix

Quand la couleur change, la galerie doit passer à la photo de cette couleur. Faites un fondu enchaîné sur place plutôt que de faire défiler toutes les images intermédiaires, et attendez que la nouvelle image soit décodée pour que le cadre ne s’affiche jamais vide. Laissez ensuite l’acheteur continuer à parcourir les autres photos. La variante décide où la galerie commence, pas où elle est bloquée.

Les prix peuvent aussi varier selon la variante. Avant une sélection complète, affichez une fourchette : « À partir de $28 ». Une fois la variante choisie, affichez son prix, son prix de référence barré si elle est en promotion, et son stock en toutes lettres : « Plus que 3 en stock », « Précommande, expédié le 12 nov. » ou « En réapprovisionnement ». Utilisez des chiffres tabulaires pour le prix afin que la ligne ne bouge pas quand les chiffres changent.

Mettre la sélection dans l’URL

?color=Oat&size=L rend une variante partageable, survit à un rechargement et permet aux publicités et aux e-mails de pointer vers le produit exact qu’un acheteur a vu. Relisez-le avec soin, en écartant tout ce que le produit n’a pas :

function selectionFromSearchParams(product: Product, params: URLSearchParams) {
  const selection: Record<string, string> = {}
  for (const { name, values } of product.options) {
    const value = params.get(name.toLowerCase())
    if (value && values.includes(value)) selection[name] = value
  }
  return selection
}

Dans Next.js, lisez searchParams dans la page pour que le serveur rende la bonne variante, et mettez à jour l’URL depuis le client quand la sélection change. Décidez si chaque changement doit ajouter une entrée dans l’historique ou remplacer l’actuelle, et tenez-vous-y.

Une checklist

  • Déduire l’état des options à partir des variantes : disponible, épuisé, indisponible.
  • Des pastilles et des boutons dans des groupes radio, avec l’état dans le nom accessible.
  • Les valeurs épuisées restent sélectionnables et proposent un e-mail de retour en stock par variante.
  • La taille n’est jamais présélectionnée, et une taille manquante est signalée et reçoit le focus.
  • Un changement de couleur qui invalide la taille la vide et explique pourquoi.
  • La galerie et le prix suivent la variante sans verrouiller la galerie.
  • La sélection vit dans l’URL.

Les blocs HextaUI pour ça

J’ai intégré ces règles au bloc Variant Picker de HextaUI Pro. Il calcule chaque option à partir des variantes que vous lui passez, garde les valeurs épuisées sélectionnables avec un formulaire NotifyMe, ne présélectionne jamais les tailles et signale une taille manquante avec « Sélectionnez une taille ». Ses helpers, findVariant, getOptionStates, getInitialSelection, getPriceRange et la paire de fonctions pour les paramètres de recherche, fonctionnent seuls, y compris sur le serveur, et fromShopify convertit un produit de la Storefront API.

Il s’associe à Product Gallery, qui fait un fondu enchaîné vers la photo d’une variante quand vous passez activeId, et à Price, qui formate n’importe quelle devise et n’anime que les chiffres qui ont changé. Les trois ont des aperçus interactifs que vous pouvez essayer avant d’acheter.

Si vous construisez le panier ensuite, le même soin s’y applique : l’UX d’un panier d’achat en React.

FAQ

Faut-il désactiver les tailles épuisées ? Non. Gardez-les sélectionnables et proposez un e-mail de retour en stock. Ne désactivez que les combinaisons qui ne sont pas fabriquées du tout.

Faut-il présélectionner une taille ? Non. Présélectionnez la couleur si vous voulez, et validez la taille quand quelqu’un ajoute au panier.

Pastilles ou liste déroulante pour les variantes ? Des pastilles et des boutons. Une liste déroulante cache le stock, qui est justement l’information que les gens cherchent.

Comment garder une variante dans l’URL ? Utilisez un paramètre de recherche par option, lisez-le sur le serveur pour le premier rendu et ignorez les valeurs que le produit n’a pas.