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
- 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-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.
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.