Tags Input
ProntoInput 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
Microinterações
| Microinteração | Disparada por | Comportamento | Timing |
|---|---|---|---|
| Tag pop-in | Enter ou , | Chip aparece com scale 0.85→1 | 200ms cubic-bezier(0.32,0.72,0,1) |
| Backspace remove | Backspace com input vazio | Remove última tag | instant |
| Click X remove | click X numa tag | Tag diminui horizontal + opacity 0 | 200ms ease |
| Suggest popup | digitando matches | Listbox abaixo do input com sugestões | 150ms fade |
| Invalid shake | tag inválida (ex: email mal-formado) | Input shake horizontal + tag não criada | 400ms |
Acessibilidade
Acessibilidade — checklist
Teclado
| Tab | Foco no input |
| Enter / , | Cria tag a partir do texto digitado |
| Backspace (input vazio) | Remove última tag |
| Tab → X | Foco em X de tag |
| Enter no X | Remove 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
| Token | Valor | Papel |
|---|---|---|
T.brassTint | rgba(164,124,43,0.08) | background da tag |
T.brassEdge | rgba(164,124,43,0.30) | borda da tag |
T.brassLo | #7D5C1D | texto da tag |
T.surface | #FFFFFF | background do container |
T.border | #E3DFD2 | borda do container |
RADIUS.pill | 980 | tag radius |