How the Interactive Exercise Browser Implements Live Search: A Client-Side JavaScript Breakdown

The interactive exercise browser implements live search entirely in client-side JavaScript inside index.html using a 250 ms debounced input listener, a pre-computed lower-case search index stored on every exercise object, and a single applyFilters() routine that refreshes the DOM instantly without any server round-trips.

The hasaneyldrm/exercises-dataset repository ships with a fully standalone interactive exercise browser that renders 1,324 exercise records without a backend. Its live search feature relies on lightweight DOM scripting rather than a remote API, and the entire filtering pipeline lives inside index.html where three coordinated stages deliver incremental results as the user types.

Debounced Input Handling

At lines 1191–1195 of index.html, the search box (#search) attaches an input listener wrapped in a debounce helper defined at lines 1208–1211.

// Debounce helper (lines 1208–1211)
function debounce(fn, ms) {
  let t;
  return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); };
}

// Input listener (lines 1191–1195)
searchEl.addEventListener('input', debounce(() => {
  state.search = searchEl.value;
  searchClearEl.classList.toggle('visible', state.search.length > 0);
  applyFilters();
}, 250));

The debounce closure resets a timer on every keystroke and only invokes the callback after the user pauses for 250 ms. This prevents the browser from re-running the full filter logic on every single input event.

Pre-Computing the Search Index

To avoid repeatedly concatenating and lower-casing object fields during each search, the application builds a flat _idx string for every exercise at initialization. This happens at lines 1226–1228 inside index.html.

// Pre-compute search index (lines 1226–1228)
state.exercises.forEach(ex => {
  ex._idx = `${ex.name} ${ex.category} ${ex.target} ${ex.equipment} ${ex.muscle_group}`.toLowerCase();
});

Each _idx value contains the exercise name, category, target muscle, equipment, and muscle group, all normalized to lower case. This design lets the filter stage perform a single String.prototype.includes() check instead of traversing nested properties on every keystroke.

Filtering and Rendering with applyFilters()

After the debounce fires, applyFilters() at lines 1292–1303 executes the actual search. It lower-cases and trims the current query, then tests every exercise against the query and any active filter chips.

// Filter routine (lines 1292–1303)
function applyFilters() {
  const q = state.search.toLowerCase().trim();
  const { category, equipment, target } = state.filters;

  state.filtered = state.exercises.filter(ex => {
    if (q && !ex._idx.includes(q)) return false;
    if (category.size  && !category.has(ex.category))   return false;
    if (equipment.size && !equipment.has(ex.equipment))  return false;
    if (target.size    && !target.has(ex.target))        return false;
    return true;
  });

  state.page = 0;
  renderGrid();
  updateResultsBar();
  updateActiveBadges();
}

If the user has typed a query, it must appear inside ex._idx. The routine also enforces any active category, equipment, or target filters stored in state.filters. Once the subset is built, the function resets pagination to page zero and refreshes the grid, results bar, and active badges in one pass.

Customizing the Live Search Pipeline

Because the interactive exercise browser live search implementation is plain JavaScript, you can extend it without adding dependencies.

Adding a Custom Search Field

Inject an additional input into the sidebar and wire it to the same debounce pattern:

// 1. Create a new input element in the sidebar
const customSearch = document.createElement('input');
customSearch.type = 'search';
customSearch.placeholder = 'Search by tag…';
customSearch.id = 'custom-search';
document.querySelector('.sidebar-body').prepend(customSearch);

// 2. Wire it up with the same debounce logic
customSearch.addEventListener('input', debounce(() => {
  const term = customSearch.value.toLowerCase().trim();
  // Search against the same `_idx` field
  state.filtered = state.exercises.filter(ex => ex._idx.includes(term));
  state.page = 0;
  renderGrid();
}, 200));

To search the English instructions field, regenerate the index with an additional template literal:

state.exercises.forEach(ex => {
  ex._idx = `${ex.name} ${ex.category} ${ex.target} ${ex.equipment} ${ex.muscle_group} ${ex.instructions?.en}`.toLowerCase();
});

Clearing Search with the Escape Key

Bind a document-level keydown handler to reset the search state when Escape is pressed:

document.addEventListener('keydown', e => {
  if (e.key === 'Escape' && state.search) {
    searchEl.value = '';
    state.search = '';
    searchClearEl.classList.remove('visible');
    applyFilters();
  }
});

Summary

  • Debounced events: The #search input uses a 250 ms debounce wrapper so filtering only runs after the user stops typing.
  • Pre-computed index: Every exercise object receives a _idx string at boot, combining name, category, target, equipment, and muscle group in lower case for fast includes() checks.
  • Synchronous filter: applyFilters() scans the master list against the text query and any active chip filters, then updates state.filtered and re-renders the UI in one pass.
  • Zero server latency: The entire interactive exercise browser live search implementation is self-contained in index.html and executes entirely inside the browser.

Frequently Asked Questions

No. According to the hasaneyldrm/exercises-dataset source code, the live search is implemented entirely in client-side JavaScript inside index.html. All 1,324 exercises are loaded as JSON, and filtering happens in memory without any network requests.

As implemented in index.html at lines 1226–1228, the _idx string includes the exercise name, category, target muscle, equipment, and muscle_group. You can extend this template literal to include additional fields such as instructions.en.

The debounce interval is hard-coded to 250 ms in the input event listener at lines 1191–1195. To adjust it, modify the second argument passed to debounce() or wrap a new listener with a different millisecond value.

Where is the main filtering logic located?

The core filter routine is applyFilters(), defined at lines 1292–1303 of index.html. This function reads state.search and state.filters, updates state.filtered, resets pagination, and triggers renderGrid() to redraw the exercise cards.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →