← Blog

UX de seletor de variantes em React: tamanhos esgotados, combinações inexistentes e volta ao estoque

Cores e tamanhos parecem simples até o estoque ficar em cada combinação. Um seletor de variantes em React para tamanhos esgotados, combinações inexistentes, fotos, preços e a URL.

Preet Suthar · · 5 min de leitura

  • React
  • E-commerce
  • UX
  • Accessibility

Uma camiseta em três cores e cinco tamanhos são quinze produtos para o seu estoque. O estoque fica em cada combinação, não em "Oat" ou "Large". A maioria dos seletores de variante ignora isso. Eles renderizam cores e tamanhos como duas listas independentes, e o cliente descobre que Oat não vem em XL quando o Adicionar à sacola falha.

É assim que se constrói um seletor de variantes em React que diz a verdade sobre o estoque: tamanhos esgotados, combinações que não existem, e-mails de volta ao estoque, e fotos e preços que seguem a escolha.

Modele as variantes, derive as opções

Comece pelas variantes, porque é isso que a sua API de e-commerce retorna e é para isso que um pedido aponta:

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[]
}

Cada valor na tela recebe um de três estados, calculado a partir das variantes e do resto da seleção atual:

  • Disponível: alguma variante com este valor e as outras opções escolhidas tem estoque.
  • Esgotado: a combinação existe, mas está sem estoque no momento.
  • Indisponível: a combinação nem é fabricada.
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"
}

Isso entrega o comportamento que os clientes esperam sem casos especiais. Uma cor continua disponível enquanto qualquer um dos tamanhos dela tiver estoque. Um tamanho só aparece riscado nas cores em que está esgotado. Escolha outra cor e os tamanhos se atualizam.

É uma função pura, então roda no servidor também. Renderize a primeira pintura com os estados certos em vez de exibir todos os tamanhos como disponíveis e depois riscar alguns.

Swatches e botões, nunca um dropdown

Um <select> esconde a única coisa que as pessoas vieram conferir: se o tamanho delas tem em estoque. Mostre as cores como swatches e os tamanhos como botões, para que o quadro completo fique visível de uma vez.

Semanticamente, cada opção é um radio group. Use inputs de rádio de verdade, ou role="radiogroup" com roving focus, para que as setas do teclado naveguem entre os valores e o grupo tenha um nome. Exponha o estado também para as tecnologias assistivas, porque uma linha riscada é invisível para um leitor de tela e fácil de passar despercebida para todo mundo:

<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>

Acima dos swatches, mostre o valor escolhido como texto ("Cor: Oat"). No hover, mostre ali o nome do valor sob o cursor, para que as pessoas não precisem adivinhar uma cor a partir de um círculo de 32px.

Esgotado ainda deve ser selecionável

O erro comum é desabilitar os tamanhos esgotados. Isso esconde a informação mais útil, quais tamanhos existem, e remove o caminho para um e-mail de volta ao estoque.

Mantenha valores esgotados selecionáveis. Quando a variante escolhida estiver esgotada, troque o Adicionar à sacola por um formulário "Avise-me quando voltar" que guarda o id da variante, não o id do produto, para que o e-mail seja sobre Oat em Large, e não sobre "esta camiseta".

Só as combinações que não existem devem ficar desabilitadas, e elas devem dizer o motivo no nome acessível: "XL, não fabricado em Oat".

Não pré-selecione o tamanho

Pré-selecionar o primeiro tamanho é como as lojas acabam despachando um monte de tamanho S. Pré-selecione a cor, já que é o que a foto já mostra, e deixe o tamanho vazio.

Quando alguém pressiona Adicionar à sacola sem um tamanho, não falhe em silêncio e não abra um alerta. Marque o grupo de tamanhos com "Selecione um tamanho", mova o foco para ele e dê uma leve tremida que o movimento reduzido desliga.

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))
}

Trocar para uma combinação que não existe

O cliente está com Large selecionado e escolhe uma cor que só vem em Small e Medium. Você tem duas opções: bloquear a cor ou aceitá-la e limpar o tamanho. Limpar o tamanho é melhor, porque a cor costuma ser a decisão maior. Mas avise. Uma live region educada com "Large não é fabricado em Rust. Selecione um tamanho." evita que usuários de leitor de tela adicionem a coisa errada, e o grupo de tamanhos visível mostra a seleção vazia para todo mundo.

Fotos e preço seguem a escolha

Quando uma cor muda, a galeria deve ir para a foto dessa cor. Faça um crossfade no lugar em vez de deslizar por todas as imagens no meio do caminho, e espere a nova imagem decodificar para que o quadro nunca pisque vazio. Deixe o cliente continuar navegando pelas outras fotos depois. A variante decide onde a galeria começa, não onde ela fica travada.

Os preços também podem variar por variante. Antes de uma seleção completa, mostre uma faixa, "A partir de $28". Depois que uma variante for escolhida, mostre o preço dela, o preço de comparação riscado se estiver em promoção, e o estoque em palavras: "Só restam 3", "Pré-venda, envio em 12 de nov." ou "Sob encomenda". Use algarismos tabulares no preço para que a linha não se desloque quando os dígitos mudarem.

Coloque a seleção na URL

?color=Oat&size=L torna uma variante compartilhável, sobrevive a um reload e permite que anúncios e e-mails apontem para o produto exato que o cliente viu. Leia-o de volta com cuidado, descartando qualquer coisa que o produto não tenha:

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
}

No Next.js, leia searchParams na página para que o servidor renderize a variante certa e atualize a URL pelo cliente quando a seleção mudar. Decida se cada mudança deve adicionar uma entrada ao histórico ou substituir a atual, e seja consistente.

Um checklist

  • Derive os estados das opções a partir das variantes: disponível, esgotado, indisponível.
  • Swatches e botões em radio groups, com o estado no nome acessível.
  • Valores esgotados continuam selecionáveis e oferecem um e-mail de volta ao estoque por variante.
  • O tamanho nunca é pré-selecionado, e um tamanho faltando é sinalizado e recebe o foco.
  • Uma troca de cor que invalida o tamanho limpa a seleção e explica o motivo.
  • A galeria e o preço seguem a variante sem travar a galeria.
  • A seleção fica na URL.

Os blocos do HextaUI para isso

Eu coloquei essas regras no bloco Variant Picker do HextaUI Pro. Ele calcula cada opção a partir das variantes que você passa, mantém valores esgotados selecionáveis com um formulário NotifyMe, nunca pré-seleciona tamanhos e sinaliza um tamanho faltando com "Selecione um tamanho". Os helpers dele, findVariant, getOptionStates, getInitialSelection, getPriceRange e o par de funções de search params, funcionam sozinhos, inclusive no servidor, e fromShopify mapeia um produto da Storefront API.

Ele combina com o Product Gallery, que faz crossfade para a foto de uma variante quando você passa activeId, e com o Price, que formata qualquer moeda e anima só os dígitos que mudaram. Os três têm previews ao vivo que você pode testar antes de comprar.

Se você vai construir o carrinho em seguida, o mesmo cuidado vale lá: UX de carrinho de compras em React.

FAQ

Tamanhos esgotados devem ficar desabilitados? Não. Mantenha-os selecionáveis e ofereça um e-mail de volta ao estoque. Desabilite só as combinações que nem são fabricadas.

Devo pré-selecionar um tamanho? Não. Pré-selecione a cor, se quiser, e valide o tamanho quando alguém adicionar à sacola.

Swatches ou dropdown para variantes? Swatches e botões. Um dropdown esconde o estoque, que é justamente a informação que as pessoas procuram.

Como eu mantenho uma variante na URL? Use um search param por opção, leia-o no servidor para a primeira renderização e ignore valores que o produto não tem.