Example Components
Filter List
A list with a live text filter. Without JavaScript the full list shows and the filter box stays hidden.
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".
true
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-widequerySelectorAll('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 aninputevent. - 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 ishiddenuntil the script runs; itsdisplay: gridwould override thehiddenattribute, so a.controls[hidden]rule restatesdisplay: 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-keywordsattribute on each item and match against both.