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
  • Dune
  • Emma
  • Beloved
  • Frankenstein
html
<filter-list data-bascik-prop-label="Filter books">
  <li>Dune</li>
  <li>Emma</li>
  <li>Beloved</li>
  <li>Frankenstein</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;
  }

  /* An author display value beats the hidden attribute, so restate it: without JavaScript the box stays hidden. */
  .controls[hidden] {
    display: none;
  }

  .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" data-testid="filter-list">
  <!-- <search> is the element HTML defines for filtering controls; the results stay outside it. -->
  <search
    class="controls"
    id="controls"
    data-testid="filter-list-search"
    hidden
  >
    <label for="query" data-bascik-prop-label>Filter</label>
    <input
      class="input"
      id="query"
      type="search"
      autocomplete="off"
      data-testid="filter-list-query"
    />
    <p class="count" id="count" aria-live="polite"></p>
  </search>
  <ul class="items" id="items" data-testid="filter-list-items">
    <li data-bascik-slot></li>
  </ul>
</div>
<script type="text/typescript">
  const query = document.getElementById("query") as HTMLInputElement;
  const count = document.getElementById("count") as HTMLElement;
  const items = [...document.getElementById("items")!.children] as HTMLElement[];

  const apply = (): void => {
    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.
  • The filter box is a <search> element, which HTML defines for filtering controls (it is a search landmark), and the list stays outside it. It is hidden until the script runs; its display: grid would override the hidden attribute, so a .controls[hidden] rule restates display: none.

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.