Filter
stimeo--filter
Shows/hides a collection of items by the set of active facet tokens.
The stimeo--filter controller filters a collection by facet tokens, decoupled from how those tokens are toggled (native checkboxes/radios, or button toggles such as stimeo--toggle-group). It reads the tokens of every control that is "on" (checked or aria-pressed="true"), then toggles each item's hidden based on its data-stimeo--filter-tokens; match (all/any) decides how multiple active tokens combine, and with none active every item is shown. Optional group containers are hidden once they hold no visible item, an optional empty element is revealed when nothing matches, and stimeo--filter:change is dispatched. State is re-derived from the live DOM on connect (Morph-safe). Behavior only — no styling is applied.
- Margherita Pizza
- Pepperoni Pizza
- Vegetable Curry
- Buffalo Wings
No dishes match the selected filters.
<%# Markup for the filter (faceted list) demo. Behavior only — no consumer JS.
The toggle-group owns the chips' pressed state + roving focus; stimeo--filter
reads the pressed tokens (data-value) and shows/hides items by their
data-stimeo--filter-tokens. match="all" means an item must carry every active
token. The empty element appears when nothing matches; Clear resets the chips. %>
<div class="filter-demo" data-controller="stimeo--filter"
data-stimeo--filter-match-value="all"
data-action="stimeo--toggle-group:change->stimeo--filter#apply">
<div class="filter-demo__bar">
<div class="filter-demo__chips" role="group"
aria-label="<%= t('components.filter.demo.legend') %>"
data-controller="stimeo--toggle-group">
<button type="button" class="filter-demo__chip" aria-pressed="false" tabindex="0"
data-value="veg"
data-stimeo--filter-target="control"
data-stimeo--toggle-group-target="item"
data-action="click->stimeo--toggle-group#toggle
keydown->stimeo--toggle-group#onKeydown">
<%= t("components.filter.demo.vegetarian") %>
</button>
<button type="button" class="filter-demo__chip" aria-pressed="false" tabindex="-1"
data-value="spicy"
data-stimeo--filter-target="control"
data-stimeo--toggle-group-target="item"
data-action="click->stimeo--toggle-group#toggle
keydown->stimeo--toggle-group#onKeydown">
<%= t("components.filter.demo.spicy") %>
</button>
</div>
<button type="button" class="filter-demo__clear" data-action="stimeo--filter#clear">
<%= t("components.filter.demo.clear") %>
</button>
</div>
<%# Each item shows its facet tokens as tags so it is clear why a chip filters it in/out. %>
<%
token_labels = {
"veg" => t("components.filter.demo.vegetarian"),
"spicy" => t("components.filter.demo.spicy"),
}
dishes = [
{ key: "margherita", tokens: "veg" },
{ key: "pepperoni", tokens: "" },
{ key: "curry", tokens: "veg spicy" },
{ key: "wings", tokens: "spicy" },
]
%>
<ul class="filter-demo__list">
<% dishes.each do |dish| %>
<li class="filter-demo__item" data-stimeo--filter-target="item"
data-stimeo--filter-tokens="<%= dish[:tokens] %>">
<span class="filter-demo__name"><%= t("components.filter.demo.#{dish[:key]}") %></span>
<span class="filter-demo__tags">
<% dish[:tokens].split.each do |tok| %>
<span class="filter-demo__tag"><%= token_labels[tok] %></span>
<% end %>
</span>
</li>
<% end %>
</ul>
<p class="filter-demo__empty" role="status" data-stimeo--filter-target="empty" hidden>
<%= t("components.filter.demo.empty") %>
</p>
</div>
/*
* Presentation-only styles for the filter (faceted list) demo.
* The library only toggles `hidden` on items / the empty element and `aria-pressed`
* (via toggle-group) on the chips; everything below is the consumer's look.
*/
.filter-demo {
display: flex;
flex-direction: column;
gap: 0.75rem;
max-width: 22rem;
}
.filter-demo__bar {
display: flex;
align-items: center;
gap: 0.5rem;
}
.filter-demo__chips {
display: inline-flex;
gap: 0.25rem;
}
.filter-demo__chip {
padding: 0.25rem 0.75rem;
color: var(--fg);
background: var(--bg);
border: 1px solid var(--border);
border-radius: 999px;
font: inherit;
font-size: 0.85rem;
cursor: pointer;
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.filter-demo__chip:hover {
border-color: var(--accent);
}
/* Pressed look is driven entirely by the library-managed aria-pressed. */
.filter-demo__chip[aria-pressed="true"] {
color: var(--white);
background: var(--accent);
border-color: var(--accent);
}
.filter-demo__clear {
margin-left: auto;
padding: 0.25rem 0.5rem;
color: var(--muted);
background: none;
border: 1px solid var(--border);
border-radius: 0.375rem;
font: inherit;
font-size: 0.85rem;
cursor: pointer;
}
.filter-demo__clear:hover {
color: var(--fg);
border-color: var(--accent);
}
.filter-demo__list {
display: flex;
flex-direction: column;
gap: 0.375rem;
margin: 0;
padding: 0;
list-style: none;
}
.filter-demo__item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
padding: 0.625rem 0.875rem;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 0.5rem;
}
.filter-demo__tags {
display: inline-flex;
gap: 0.25rem;
flex: none;
}
/* The item's facet tokens, shown so it is clear why a chip filters it in or out. */
.filter-demo__tag {
padding: 0.05rem 0.45rem;
color: var(--muted);
background: var(--sidebar-bg);
border: 1px solid var(--border);
border-radius: 999px;
font-size: 0.7rem;
}
/* The library hides non-matching items via the `hidden` attribute; the shared
base reset (base/reset.css) enforces `[hidden] { display: none !important }`,
so no per-demo re-declaration is needed. */
.filter-demo__empty {
margin: 0;
padding: 0.75rem 0.875rem;
color: var(--muted);
border: 1px dashed var(--border);
border-radius: 0.5rem;
}
This demo needs no consumer-side JS (the controller handles the behavior).
These demo styles use shared design tokens (light + dark). Copy the shared styles too, then toggle data-theme on your root element for dark mode.
The data-* attributes you add to your own HTML to wire this component. Put the data-controller below on a root element, then place its targets / values / actions inside that element.
On the root element
data-controller="stimeo--filter"
Targets
| Name | Description | Attribute |
|---|---|---|
item
required
|
Element shown/hidden by its data-stimeo--filter-tokens. |
data-stimeo--filter-target="item" |
control
|
Element whose on-state (checked / aria-pressed) contributes its token. |
data-stimeo--filter-target="control" |
group
|
Optional container hidden once it holds no visible item. | data-stimeo--filter-target="group" |
empty
|
Optional element revealed when nothing matches. | data-stimeo--filter-target="empty" |
Values
| Name | Description | Attribute |
|---|---|---|
match
|
How active tokens combine: all (AND, default) or any (OR). |
data-stimeo--filter-match-value |
Actions
| Name | Description | Action |
|---|---|---|
apply
|
Re-evaluates visibility (wire button toggles' events here). | stimeo--filter#apply |
clear
|
Turns every control off and re-applies. | stimeo--filter#clear |
Events
| Name | Description | Event |
|---|---|---|
change
|
Fires on every evaluation, with detail.active / detail.visible / detail.total. |
stimeo--filter:change |
State hooks
The library only manages these ARIA/data attributes and custom properties. Your CSS reads them to render the look — selectors like [aria-selected], [aria-expanded], or var(--stimeo--…) hook into this state.
| Hook | Target | Meaning |
|---|---|---|
hidden |
Item / group / empty | Toggled on non-matching items, empty groups, and the empty element. |