Como usar o campo nativo de busca e filtros sem dependências externas

1. Fundamentos da busca nativa no navegador

A API nativa do navegador oferece eventos como input e change para capturar consultas em tempo real. A principal diferença entre busca local (client-side) e remota (server-side) está no local onde a filtragem ocorre: na busca local, todo o conjunto de dados já está carregado no navegador, enquanto na remota, cada consulta dispara uma requisição ao servidor.

Para acessibilidade, utilize atributos como aria-label no campo de busca, role="search" no formulário e gerencie o foco automático com autofocus ou element.focus().

<!-- Exemplo básico de campo de busca acessível -->
<form role="search" aria-label="Busca de temas">
  <input
    type="search"
    id="campoBusca"
    aria-label="Digite o nome do tema"
    placeholder="Buscar tema..."
    autofocus
  />
</form>

2. Estrutura HTML semântica para campo de busca e filtros

Construa um formulário semântico usando <input type="search"> para o campo principal e <datalist> para sugestões pré-definidas. Organize filtros com <fieldset> e <legend> para agrupar opções relacionadas.

<form role="search" aria-label="Filtros de temas">
  <fieldset>
    <legend>Filtros disponíveis</legend>

    <label for="busca">Buscar tema:</label>
    <input type="search" id="busca" list="sugestoes" />
    <datalist id="sugestoes">
      <option value="Tecnologia">
      <option value="Saúde">
      <option value="Educação">
    </datalist>

    <label for="categoria">Categoria:</label>
    <select id="categoria">
      <option value="">Todas</option>
      <option value="ciencia">Ciência</option>
      <option value="arte">Arte</option>
      <option value="historia">História</option>
    </select>

    <label>
      <input type="checkbox" id="apenasFavoritos" />
      Apenas favoritos
    </label>
  </fieldset>

  <div aria-live="polite" id="resultados">Nenhum resultado ainda</div>
</form>

3. Lógica de filtragem com JavaScript puro: algoritmos básicos

Implemente busca textual com String.prototype.includes() combinado com toLowerCase() para tornar a busca case-insensitive. Para múltiplos filtros, utilize operadores lógicos AND/OR com arrays e o método filter().

const temas = [
  { id: 1, nome: "Inteligência Artificial", categoria: "tecnologia" },
  { id: 2, nome: "Medicina Preventiva", categoria: "saude" },
  { id: 3, nome: "Arte Renascentista", categoria: "arte" }
];

function filtrarTemas(termoBusca, categoria, apenasFavoritos) {
  return temas.filter(tema => {
    const nome = tema.nome.toLowerCase();
    const termo = termoBusca.toLowerCase();
    const correspondeBusca = nome.includes(termo);
    const correspondeCategoria = !categoria || tema.categoria === categoria;
    return correspondeBusca && correspondeCategoria;
  });
}

Para tratamento de acentos, utilize normalize('NFD').replace(/[\u0300-\u036f]/g, '') antes da comparação.

4. Otimização de performance para grandes listas

Implemente debouncing para evitar execuções excessivas a cada tecla pressionada. Uma função debounce atrasa a execução até que o usuário pare de digitar por um intervalo definido.

function debounce(fn, delay = 300) {
  let timer;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

const buscarComDebounce = debounce((evento) => {
  const termo = evento.target.value;
  const resultados = filtrarTemas(termo, '', false);
  exibirResultados(resultados);
}, 400);

Para grandes listas (acima de 10.000 itens), limite a exibição com slice() e implemente scroll manual para evitar travamentos na interface.

5. Filtros avançados com expressões regulares e dados aninhados

Use RegExp para padrões complexos de busca, como palavras que começam com determinada letra ou que contenham padrões específicos.

function buscarComRegex(termo, lista) {
  const regex = new RegExp(termo, 'gi');
  return lista.filter(item => regex.test(item.nome));
}

// Exemplo: buscar temas que começam com "A"
const temasComA = buscarComRegex('^a', temas);

Para dados aninhados, utilize reduce() com recursão controlada para percorrer objetos complexos:

function buscarEmObjeto(objeto, termo) {
  return Object.values(objeto).reduce((resultados, valor) => {
    if (typeof valor === 'object') {
      return resultados.concat(buscarEmObjeto(valor, termo));
    }
    if (String(valor).toLowerCase().includes(termo.toLowerCase())) {
      resultados.push(objeto);
    }
    return resultados;
  }, []);
}

6. Gerenciamento de estado e reatividade sem frameworks

Armazene o estado de busca e filtros em um objeto central (state) e atualize a interface com funções puras utilizando documentFragment para evitar múltiplas manipulações do DOM.

const state = {
  termoBusca: '',
  categoria: '',
  apenasFavoritos: false,
  resultados: []
};

function atualizarEstado(novoEstado) {
  Object.assign(state, novoEstado);
  renderizarResultados();
}

function renderizarResultados() {
  const fragment = document.createDocumentFragment();
  state.resultados.forEach(tema => {
    const div = document.createElement('div');
    div.textContent = tema.nome;
    fragment.appendChild(div);
  });
  const container = document.getElementById('resultados');
  container.innerHTML = '';
  container.appendChild(fragment);
}

Para persistência temporária, utilize sessionStorage:

function salvarEstado() {
  sessionStorage.setItem('filtrosTemas', JSON.stringify(state));
}

function carregarEstado() {
  const salvo = sessionStorage.getItem('filtrosTemas');
  if (salvo) Object.assign(state, JSON.parse(salvo));
}

7. Feedback visual e tratamento de casos extremos

Exiba "Nenhum resultado encontrado" quando a filtragem retornar vazio, e mostre a contagem de itens encontrados.

function exibirResultados(lista) {
  const container = document.getElementById('resultados');
  if (lista.length === 0) {
    container.innerHTML = '<p role="alert">Nenhum resultado encontrado</p>';
    return;
  }
  container.innerHTML = `<p>${lista.length} tema(s) encontrado(s)</p>`;
  // Adicionar itens...
}

Para indicadores de carregamento em buscas assíncronas, utilize um spinner CSS simples:

<spinner class="spinner" aria-hidden="true" style="display:none;"></spinner>

<style>
.spinner {
  width: 24px;
  height: 24px;
  border: 3px solid #ccc;
  border-top-color: #333;
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}
@keyframes spin {
  to { transform: rotate(360deg); }
}
</style>

Implemente um botão "Resetar" que limpa todos os filtros e restaura a lista original:

document.getElementById('resetar').addEventListener('click', () => {
  document.getElementById('busca').value = '';
  document.getElementById('categoria').value = '';
  document.getElementById('apenasFavoritos').checked = false;
  state.termoBusca = '';
  state.categoria = '';
  state.apenasFavoritos = false;
  state.resultados = [...temas];
  renderizarResultados();
});

8. Testes e depuração da busca nativa

Simule eventos de teclado e clique com dispatchEvent para testar automaticamente a lógica de busca:

function simularDigitacao(elemento, texto) {
  elemento.value = texto;
  elemento.dispatchEvent(new Event('input', { bubbles: true }));
}

const campo = document.getElementById('busca');
simularDigitacao(campo, 'Inteligência');

Meça o tempo de filtragem com performance.now() para identificar gargalos:

const inicio = performance.now();
const resultados = filtrarTemas(termo, categoria, favoritos);
const fim = performance.now();
console.log(`Tempo de filtragem: ${fim - inicio}ms`);

Teste a acessibilidade garantindo que leitores de tela anunciem as mudanças nos resultados através do aria-live e que o foco seja gerenciado corretamente após cada filtragem.

Referências