Search and Filter Cards
Combine a search box with auto-generated checkbox and radio filters. The component scans card content and builds filter options automatically from data attributes.
Auto-Generated Filter Types
Four auto-generated filter input types: checkbox (multi-select, OR logic), radio (single-select), switch (toggle, OR logic), and slider (numeric range). The first three build their options from card content; the slider reads its bounds from data-filter-min/data-filter-max.
Collapsible Filter Groups
Add data-filter-accordion to a filter group and it renders as a collapsible Accordion item instead of an always-open fieldset. Opt in per group: reach for it when a group has many options and would otherwise push the rest of the menu out of view. Groups without the attribute stay inline, so short groups keep their options visible at a glance.
Filter Bar (Standard Layout)
The standard arrangement: a Toolbar directly above the grid, holding the search box, Filter, applied-filter chips, and auto-fill suggestions. Each surface carries its own data-filter-target, so the bar itself stays pure layout. The Filter here pairs a category checkbox group with a price slider (data-filter-type="slider"). Sort controls can live in the bar too: see the Sort page. Grid and pagination stay independent siblings below.
Explicit Values and Label Mapping
Define filter options upfront with data-filter-values instead of scanning card content. Pass a JSON object {"value":"label"} to map machine values to display labels, keeping internal identifiers separate from what users see.
Dynamic Values (populateFilter API)
Use populateFilter() to generate filter inputs from values fetched at runtime. Supports cascading filters where one filter's selection determines another filter's options.
Place an empty data-filter placeholder in the dropmenu, then call populateFilter() after fetching values. The method generates the same auto-generated inputs as data-filter-type and binds all listeners automatically.
<!-- Empty placeholder — JS will generate the inputs -->
<div class="nds-dropmenu nds-filter" id="apiFilter"
data-filter-target="results">
<button class="nds-btn nds-neutral nds-filter-btn nds-dropmenu-trigger">
<i class="hgi hgi-stroke hgi-filter"></i>
<span class="nds-label" data-hidden="sm sr">Filter</span>
</button>
<div class="nds-dropmenu-menu" hidden>
<div class="nds-dropmenu-scroll">
<div data-filter="system"
data-filter-type="checkbox"
data-filter-legend="System"
data-no-auto-close>
</div>
</div>
<div class="nds-dropmenu-footer">
<hr class="nds-divider">
<div class="nds-dropmenu-action">
<button class="nds-btn nds-secondary nds-dropmenu-item"
type="button" data-filter-action="clear"
data-no-auto-close>
<span class="nds-label">Reset</span>
</button>
<button class="nds-btn nds-primary nds-dropmenu-item"
type="button" data-filter-action="apply">
<span class="nds-label">Apply</span>
</button>
</div>
</div>
</div>
</div>
// Fetch values from API, then populate the filter
NDS.Filter.whenReady('#apiFilter', (filter) => {
NDS.request('/api/systems', { json: true })
.then(({ data }) => {
filter.populateFilter('system', data.map(d => d.Title));
});
});
Call populateFilter() again whenever a parent filter changes. The method clears the previous inputs and generates new ones from the updated values.
// Cascading: when beneficiary changes, re-populate system filter
NDS.Filter.whenReady('#apiFilter', (filter) => {
const beneficiaryInputs = document.querySelectorAll(
'input[name="beneficiary"]'
);
beneficiaryInputs.forEach(radio => {
radio.addEventListener('change', () => {
NDS.request('/api/systems?userIds=' + radio.value, { json: true })
.then(({ data }) => {
filter.populateFilter(
'system',
data.map(d => d.Title)
);
});
});
});
});
AJAX Form Submission
Send filter criteria to a server endpoint via AJAX. HTML responses are auto-injected into the target container — the response must contain an element with the target's id, or the submission is treated as a failure and the existing results are left in place. JSON responses dispatch raw data via event for developer rendering.
Add a separate <form> element with data-filter-target linking it to the filter anchor, plus data-filter-submit and data-ajax attributes. Set the action attribute to the API endpoint URL.
.nds-filter stays a pure anchor — the form drives submission. HTML responses are automatically injected into the target container. For JSON responses, listen for the nds:filterFormComplete event and render the data yourself.
Use preventDefault() on the nds:filterFormAjax event to fully control the AJAX request and rendering. The filter component still handles UI updates (chips, count, URL params) before dispatching the event — so if your request fails, call e.detail.rollback() to put them back rather than leaving them describing results that were never rendered.
All filter actions (apply, chip removal, reset, clear) fire through nds:filterFormAjax, so you only need one event listener.
// Intercept AJAX and handle fetching yourself
// Covers: apply, chip removal, reset, and clear
filterForm.addEventListener('nds:filterFormAjax', (e) => {
e.preventDefault();
// Build your own params from form inputs
const params = {};
const search = filterForm.querySelector('input[name="search"]');
if (search && search.value) params.q = search.value;
// Chips, badge and URL params are already committed by the time this
// fires. If your request fails they describe results that were never
// rendered — detail.rollback() puts them back.
NDS.request('/api/search', {
method: 'POST',
body: new URLSearchParams(params),
json: true
})
.then(({ data }) => renderResults(data.Records))
.catch(() => e.detail.rollback());
});
Built-in Features
Every element that carries the same data-filter-target joins one filter: search box, dropmenu, chips row, count slots. A .nds-filter element is optional, so a search box on its own is already a working filter.
Builds checkbox, radio, or switch inputs automatically. Values come from card content, a JSON attribute (data-filter-values), or the populateFilter() API — no manual HTML required.
Add data-search to the filter dropmenu and a search box appears above the options. Every generated checkbox, radio, and switch row is filtered as the user types.
Set data-filter-type="slider" to inject a range (dual-thumb) or "up to" (single-thumb) slider that filters cards by a numeric data-filter-value. The active selection shows as one removable chip and syncs to the URL.
Add data-filter-accordion to a long filter group and it becomes a collapsible section, closed by default, with a tag on its header counting the values selected inside. Short groups stay inline.
Selections and the search term sync to URL query parameters, so a filtered view is bookmarkable and restores exactly. Checkbox and switch groups join values with commas (?department=Design,Finance), so their option values must not contain one. Radio values may.
Active filters display as removable chips below the filter bar. Clicking a chip removes that filter and re-applies the remaining criteria.
Shows a warning alert with a "Clear Filter" action when no cards match the current criteria. The alert dismisses automatically when results reappear.
Use populateFilter() to generate or replace filter inputs at runtime. Supports cascading filters where one selection drives another filter's options via API.
Supports server-side filtering via AJAX with automatic HTML response injection and JSON response events for custom rendering.
Filter any element type by setting data-filter-items on the target container. Works with list items, table rows, drawers, or any custom structure beyond the default .nds-card.
Separate machine values from display labels using data-filter-value on items or the object form of data-filter-values on filter groups. Labels are derived automatically from visible text content.
Options for a group that sits inside a closed dropmenu are built on the first open, not at page load, so a long option list costs nothing until the user asks for it. Groups whose values arrive in the URL still build on load.
Set filters, search terms, and reset state through the NDS.Filter API. Access instances by selector, target ID, or the whenReady helper.
Usage Guidelines
Best Practices
- Use client-side filtering when all items are already on the page and the dataset is small enough to load at once (under a few hundred cards)
- Use AJAX form submission mode (
data-filter-submit+data-ajax) for large datasets or when results come from an API endpoint - Use auto-generated filters (
data-filter-type) for quick setup when filter values come directly from card content. Usedata-filter-valuesto supply explicit values when cards don't exist or values differ from card content. UsepopulateFilter()for dynamic or cascading values fetched at runtime - Do not use Filter for navigation menus or hierarchical browsing. Use Side Nav or Tabs instead
- Do not use Filter for single-field search without filter controls. Use the search box from Forms directly
- Choose checkbox for multi-select with OR logic, radio for mutually exclusive single-select, and switch for feature toggles where each option is independent
- Choose slider for a continuous numeric facet (price, distance, area): both bounds give a dual range,
data-filter-maxalone gives an "up to" thumb. Give each card a numericdata-filter-valueon itsdata-filtermarker; the visible text can still readSAR 250while the value stays a bare number - Combine a search box with filter controls for the best experience. Search narrows by text while filters narrow by category
- Always include a Reset/Clear button inside the dropmenu footer so users can undo selections before applying
- Add the
.nds-filter-appliedcontainer to show applied filter chips. This gives users visibility into active filters and a quick way to remove individual ones - Keep filter group names short and descriptive. The
data-filter-legendvalue appears as the fieldset heading inside the dropmenu - Add
data-filter-accordionto groups with many options so the menu opens on a short list of headers rather than a long scroll. Leave short groups (three or four options) inline: collapsing them hides choices behind a click for no gain
Structural Classes
| Class | Description |
|---|---|
nds-filter | The filter anchor. Add it next to nds-dropmenu for the standard Filter button and menu. |
nds-filter-btn | Marks the always-visible trigger button. It carries the applied-filter count badge and the loading spinner during a submission. |
nds-filter-applied | The applied-chips row. Give it an inner .nds-chips element and the filter fills it. |
nds-auto-fill | A suggestion row shown only while no filter is applied. Same label plus chips layout as the applied row. |
nds-filter-menu | Added by the filter to its own .nds-dropmenu-menu. Style the menu through this class: it stays on the menu after data-portal moves it. |
nds-filter-range | The fieldset a slider filter generates. Read-only hook for styling. |
Data Attributes
Filter Anchor (.nds-filter)
| Attribute | Description |
|---|---|
data-filter-target | ID of the container holding filterable items. Also used to link the anchor to its submission form, search box, applied-chips row, query/count slots, and filter controls. |
data-search | When the anchor is also a Dropmenu, adds a search box above the options and filters the generated rows as the user types. Pass a number (data-search="50") to show it only once the menu holds that many options. |
data-portal | Moves the open menu to <body>. Use it when the filter sits inside a modal, drawer, or any scrolling box that would clip the menu. Width knobs travel with the menu. |
Submission Form (separate <form data-filter-target>)
| Attribute | Description |
|---|---|
data-filter-target | Must match the anchor's target id to activate form mode for that filter instance. |
data-filter-submit | Marks this form as the submission form (enables form mode instead of client-side filtering). |
data-ajax | Use AJAX instead of page navigation (requires data-filter-submit). |
Search Input Opt-Out
| Attribute | Description |
|---|---|
data-filter-ignore | Place on a search input (or its ancestor) to prevent the filter from auto-detecting and hijacking it. Useful when a server-side search input lives inside the filter scope but should not be used for client-side text filtering. |
Target Container
| Attribute | Description |
|---|---|
data-filter-items | Set on the target container (the element referenced by data-filter-target) to specify which descendants are filterable. Canonical form is a bare class name, e.g. data-filter-items="search-result"; a tag name (tr) or any CSS selector (.nds-card, [data-row]) also works. Default: .nds-card. On a <tbody> the match is narrowed to the rows that body owns, so data-filter-items="tr" needs no guard against a nested table's rows or a nds-sub detail row. Other containers keep the full descendant match, where a wrapper between the container and its items is normal. Setting the attribute (even with the default value) also opts the container into the critical-CSS hold: the container stays hidden until the filter initializes and has applied any URL filter params — so a URL-filtered page never flashes the unfiltered list. |
data-total-count | Set on the target container by server-side rendering or inside a nds:filterFormComplete handler to provide a server-authoritative result count. When present, overrides the DOM-enumerated count written to [data-filter-count] slots. |
Result Count and Query Slots
| Attribute | Description |
|---|---|
data-filter-count | Place on any element linked via data-filter-target. The filter writes the number of visible items into this element's textContent after every filter pass. Pair with .nds-bar-text for the standard styling. For lists that also paginate, prefer the Pagination records counter (data-paged-target): its count is the filtered count and it adds the "showing x to y" window. |
data-filter-query | Place on any element linked via data-filter-target. The filter writes the active search keyword (wrapped in curly quotes) into this element's textContent. When present, the search term is routed here instead of appearing as an applied-chip. |
Filter Groups
| Attribute | Description |
|---|---|
data-filter="name" | Filter group name. On filter controls, groups inputs together. On item elements, marks filterable content. Can be placed on child elements inside items or on the item itself. |
data-filter-type | Auto-generate inputs. Values: checkbox, radio, switch, or slider. The first three scan cards for values unless data-filter-values is set; radio groups auto-prepend an "All" option (selected by default) so the filter can be cleared. slider injects a slider and matches each card's numeric data-filter-value: both data-filter-min + data-filter-max give a dual-thumb range, data-filter-max alone gives a single "up to" thumb. |
data-filter-min, data-filter-max | Slider only. The numeric bounds. Both present means a dual range; data-filter-max alone (floor defaults to 0) means a single "up to" thumb. max must be greater than min. |
data-filter-step | Slider only. Snap increment for the thumb(s). Default: 1. |
data-filter-currency | Slider only. Currency code (e.g. SAR) shown on the slider value outputs and the applied-filter chip via number formatting. |
data-filter-unit | Slider only. A text unit (e.g. km, years, %) appended after the value on the outputs and chip; the non-currency counterpart of data-filter-currency. |
data-filter-all-label | Override the auto-prepended "All" label on radio groups. Default: الكل in Arabic, All otherwise. |
data-filter-no-all | Opt out of the auto-prepended "All" option on radio groups (boolean attribute). |
data-filter-values | JSON object mapping machine values to display labels, e.g. '{"A":"Label A","B":"Label B"}'. Keys become checkbox/radio values, values become visible text. Also accepts a JSON array ('["A","B"]') which uses raw values as labels. Skips card scanning. Static: not affected by refresh(). Use populateFilter() if values need to change at runtime. Requires data-filter-type. |
data-filter-legend | Fieldset legend text for auto-generated filter groups |
data-filter-accordion | Boolean attribute. Renders this group as a collapsible Accordion item, closed by default, with the data-filter-legend text as the header and a tag counting that group's selected values (hidden at zero). Opt in per group: groups without it stay inline. Wrap several opted-in groups in your own <div class="nds-accordion"> to make them one accordion; otherwise each group becomes its own, so they open independently. |
data-filter-variant | CSS class to add to auto-generated input elements (e.g. nds-primary) |
data-filter-value | Set on a [data-filter] element to provide a machine-readable filter value separate from the visible text. The display label is derived from the element's text content automatically. Example: <span data-filter="type" data-filter-value="Announcement">Translated Label</span> |
Action Buttons
| Attribute | Description |
|---|---|
data-filter-action="apply" | Apply current filter selections and close the dropmenu |
data-filter-action="clear" | Reset all filter inputs in the dropmenu without closing it |
data-filter-action="reset" | Clear all filters, search, and chips, and show all items |
Applied Filters Container
| Attribute | Description |
|---|---|
data-chip-class | Set on .nds-filter-applied to customize chip styling. Default: nds-primary nds-lg |
Auto-Fill Container
| Class / Attribute | Description |
|---|---|
.nds-auto-fill | Place on any element linked via data-filter-target. The filter detects it by class and automatically hides it when any filters are applied, then shows it again when all filters are cleared. Use it for promotional or instructional content that should only appear before the user has filtered. |
Search Suggestions
Typed suggestions in the search box are owned by Autocomplete, not by Filter. Put these on the same .nds-form-container that holds the search input and both components work together. See the Autocomplete page for the full list.
| Attribute | Description |
|---|---|
data-url | API endpoint that returns the suggestions. |
data-name | JSON field to display from each result. Default: Title. |
data-query-param | Query parameter name for the typed term. Default: q. |
CSS Custom Properties
| Property | Default | Description |
|---|---|---|
--dropmenu-min-width | 250px | Minimum width of the filter menu. Set it on the .nds-filter element, not on the menu: the menu keeps the value even after data-portal moves it to <body>. |
State and Status (form submission mode)
In form mode, the filter sets these attributes on the .nds-filter anchor element via NDS.State and NDS.Status. These drive the built-in SCSS rules below.
| Selector | Effect | When set |
|---|---|---|
.nds-filter[data-state~="submitting"] | pointer-events: none | Set on the anchor when a standard or AJAX form submission is in flight. Cleared when the response arrives. |
.nds-filter[data-status="success"] | Search inputs get border-color: var(--border-success) | Set on successful AJAX response. Auto-cleared after 3 seconds. |
.nds-filter[data-status="error"] | Search inputs get border-color: var(--border-error) | Set on AJAX request failure. Auto-cleared after 5 seconds. |
Because a failed submission deliberately leaves the results untouched, the border tint is the only thing on screen that moves — so filter also raises an error toast via Alert (soft dependency: skipped if nds-alert.js isn't bundled). Call preventDefault() on nds:filterFormError to suppress it.
A failure also rolls the applied state back to what the displayed results represent: chips, the filter-button badge, the dropmenu controls and the URL params all return to their pre-submission values. Without it a failed Clear would show no chips over results that are still filtered. The trade-off is that an unsaved selection made in the dropmenu is discarded along with the failed submission.
Keyboard and Accessibility
Enterin the page search box runs the search straight awayEnteranywhere inside the open filter menu triggers the Apply button, so a keyboard user never has to tab to it- Generated options are real
<input>elements inside a<fieldset>with a legend, so screen readers announce the group name with each option - Radio groups get an "All" option first. Without it a keyboard user could pick a value but never clear it
- Slider thumbs carry their own labels and respond to the arrow keys
JavaScript API
The NDS.Filter API provides methods to create, query, and control filter instances programmatically. For dynamically added filter forms, call NDS.Filter.init() to initialize new instances.