Filter List

A list with a live text filter. Without JavaScript the full list shows and the filter box stays hidden.

Source on GitHub

Usage

Pass <li> items in the default slot; the slot sits inside the component's <ul>. label labels the filter box and defaults to "Filter".

examples/components/filter-list/filter-list.html
  • HTML
  • CSS
  • JavaScript
  • TypeScript
html
<filter-list data-bascik-prop-label="Filter languages">
  <li>HTML</li>
  <li>CSS</li>
  <li>JavaScript</li>
  <li>TypeScript</li>
</filter-list>
html examples/components/filter-list/filter-list.html
<!-- @bascik
A list with a live text filter. Pass <li> items in the default slot. Without JavaScript the full list shows and the filter box stays hidden.
@prop label - Label for the filter box. Defaults to "Filter".
@slot default - The <li> items to filter.
-->
<style>
  .filter-list {
    display: grid;
    gap: 0.5rem;
  }

  .controls {
    display: grid;
    gap: 0.25rem;
  }

  .input {
    padding: 0.5rem 0.75rem;
    border: 1px solid var(--color-border, color-mix(in srgb, currentColor 30%, transparent));
    border-radius: var(--radius, 0.5rem);
    background: var(--color-bg, Field);
    color: inherit;
    font: inherit;
  }

  .input:focus-visible {
    outline: 2px solid var(--color-accent, currentColor);
    outline-offset: 1px;
  }

  .items {
    margin: 0;
    padding: 0.75rem 0.75rem 0.75rem 2rem;
    border-radius: var(--radius, 0.5rem);
    background: var(--color-surface, color-mix(in srgb, currentColor 8%, transparent));
  }

  .count {
    margin: 0;
    color: var(--color-muted, inherit);
    font-size: 0.875em;
  }
</style>
<div class="filter-list">
  <div class="controls" id="controls" hidden>
    <label for="query" data-bascik-prop-label>Filter</label>
    <input class="input" id="query" type="search" autocomplete="off" />
    <p class="count" id="count" aria-live="polite"></p>
  </div>
  <ul class="items" id="items">
    <li data-bascik-slot></li>
  </ul>
</div>
<script>
  const query = document.getElementById('query');
  const count = document.getElementById('count');
  const items = [...document.getElementById('items').children];

  const apply = () => {
    const needle = query.value.trim().toLowerCase();
    let shown = 0;
    for (const item of items) {
      const match = item.textContent.toLowerCase().includes(needle);
      item.hidden = !match;
      if (match) shown++;
    }
    count.textContent = needle ? `${shown} of ${items.length} shown` : '';
  };

  query.addEventListener('input', apply);
  // The browser may restore a typed value on Back or reload without firing input.
  window.addEventListener('pageshow', apply);
  document.getElementById('controls').hidden = false;
</script>
Output is shown unminified (HTML, CSS, JS, and identifier names) for readability.

How It Works

  • The script reaches the items through the list it found by ID (document.getElementById('items').children). A page-wide querySelectorAll('li') would also match every other instance's items.
  • Matching is case-insensitive on each item's text. A polite live region reports how many items are shown.
  • The filter runs again on pageshow, because Back and reload can restore the input's value without an input event.

Adapt It

  • For long lists, debounce the input and filter a data array instead of the DOM. See Interactivity.
  • To filter on more than the visible text, put keywords in a data-keywords attribute on each item and match against both.