Tags Input

Pronto

Input que cria tags ao digitar Enter ou vírgula. Cada tag vira chip removível inline. Ideal para lista de termos editável.

Usar quando

Termos monitorados (OAB), tags de processo, recipientes de email, palavras-chave de busca, áreas de interesse.

Não usar quando

Lista fechada (use Combobox multi). Texto livre (use Textarea). Items < 3 (use checkboxes).

Variantes

CívelTrabalhista
Cível
paulo@oggma.com
SPRJ

Microinterações

MicrointeraçãoDisparada porComportamentoTiming
Tag pop-inEnter ou ,Chip aparece com scale 0.85→1200ms cubic-bezier(0.32,0.72,0,1)
Backspace removeBackspace com input vazioRemove última taginstant
Click X removeclick X numa tagTag diminui horizontal + opacity 0200ms ease
Suggest popupdigitando matchesListbox abaixo do input com sugestões150ms fade
Invalid shaketag inválida (ex: email mal-formado)Input shake horizontal + tag não criada400ms

Acessibilidade

Acessibilidade — checklist

Teclado
TabFoco no input
Enter / ,Cria tag a partir do texto digitado
Backspace (input vazio)Remove última tag
Tab → XFoco em X de tag
Enter no XRemove a tag
ARIA esperado
  • role="combobox" aria-multiselectable="true"
  • Tags como <ul role="list"><li>
  • aria-label="Adicionar tag"
  • Live region: aria-live="polite" para anunciar tag adicionada/removida
Notas
  • Backspace no input vazio remove última tag (esperado em mailto, etc).
  • Vírgula como delimitador também é padrão.
  • Validação por tag (não por input) — tag inválida não é criada.
  • Limite max retorna feedback visual.

Código

'use client';
import { useState } from 'react';
import { X } from 'lucide-react';

export function TagsInput({ value, onChange, placeholder, validate }) {
  const [text, setText] = useState('');
  function add(t: string) {
    t = t.trim();
    if (!t) return;
    if (validate && !validate(t)) return;
    if (value.includes(t)) return;
    onChange([...value, t]);
    setText('');
  }
  return (
    <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, padding: 6 }}>
      {value.map((tag) => (
        <Chip key={tag} removable onRemove={() => onChange(value.filter(v => v !== tag))}>{tag}</Chip>
      ))}
      <input
        value={text}
        onChange={(e) => setText(e.target.value)}
        onKeyDown={(e) => {
          if (e.key === 'Enter' || e.key === ',') { e.preventDefault(); add(text); }
          if (e.key === 'Backspace' && !text) onChange(value.slice(0, -1));
        }}
        placeholder={placeholder}
      />
    </div>
  );
}

Regras

Faça

  • Enter e vírgula como delimitadores.
  • Backspace remove última tag (input vazio).
  • Validação por tag — inválida não cria.
  • Limite max com feedback.
  • Tags duplicadas ignoradas (não erro silencioso).
  • Sugestões via popup se houver lista de hints.

Não faça

  • Não use sem delimitador (texto vira só string).
  • Não permita tag vazia.
  • Não silencie validação inválida (mostre erro).
  • Não esconda backspace = remove (atalho clássico).
  • Não exiba 100+ tags sem scroll/wrap apropriado.

Tokens usados

TokenValorPapel
T.brassTintrgba(164,124,43,0.08)background da tag
T.brassEdgergba(164,124,43,0.30)borda da tag
T.brassLo#7D5C1Dtexto da tag
T.surface#FFFFFFbackground do container
T.border#E3DFD2borda do container
RADIUS.pill980tag radius