v1.6.x-dev NDS IQ v6

Filter - National Design System

A flexible filtering system for narrowing down content using search and auto-generated filter controls. Supports client-side card filtering, static or dynamic filter values, and AJAX form submission

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.

Search + Auto Checkbox Filter
Ahmed Al-Rashidi Senior Developer
Engineering Senior Developer
Fatima Al-Harbi UX Designer
Design UX Designer
Sara Al-Dosari Marketing Lead
Marketing Marketing Lead
Layla Al-Qahtani HR Specialist
Human Resources HR Specialist
Khalid Al-Otaibi Project Manager
Engineering Project Manager
Omar Al-Shahrani Data Analyst
Finance Data Analyst
Mohammed Al-Zahrani Security Engineer
Engineering Security Engineer
Noura Al-Ghamdi Content Strategist
Marketing Content Strategist
Abdulaziz Al-Shehri Finance Manager
Finance Finance Manager
Hana Al-Mutairi Frontend Developer
Engineering Frontend Developer
Turki Al-Subaie Operations Lead
Human Resources Operations Lead
Reem Al-Tamimi Visual Designer
Design Visual Designer
<!-- One flex bar above the grid; each surface is wired by its own data-filter-target --> <div class="nds-toolbar"> <!-- Search box (field + button) --> <div class="nds-form-container nds-search-box" data-filter-target="basicFilterCards"> <div class="nds-search-content"> <div class="nds-form-control"> <i class="nds-icon nds-hgi-search-01" aria-hidden="true"></i> <input type="text" class="nds-search-input" placeholder="Search cards..."> <div class="nds-form-action"> <button class="nds-btn nds-subtle nds-clear" hidden> <i class="nds-icon nds-hgi-cancel-01" aria-hidden="true"></i> </button> </div> </div> <button class="nds-btn nds-primary nds-search-btn" type="button"> <i class="nds-icon nds-hgi-search-01" aria-hidden="true"></i> <span class="nds-label" data-hidden="sm sr">Search</span> </button> </div> </div> <!-- Filter: sibling of the search box, not nested inside it --> <div class="nds-dropmenu nds-filter" data-filter-target="basicFilterCards"> <button class="nds-btn nds-neutral nds-menu-btn 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="department" data-filter-type="checkbox" data-filter-legend="Department" data-no-auto-close> </div> <hr class="nds-divider"> <div data-filter="role" data-filter-type="radio" data-filter-legend="Role" 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">Filter</span> </button> </div> </div> </div> </div> <div class="nds-filter-applied" data-filter-target="basicFilterCards" hidden> <span class="nds-label">Applied Filters:</span> <div class="nds-chips"></div> </div> </div> <!-- Filterable Cards --> <div id="basicFilterCards" class="nds-paged-content nds-grid" data-filter-items="nds-card" style="--per-page: 6; --max-col: 3; --mid-col: 2; --min-col: 1;"> <div class="nds-card nds-stroke nds-page-item"> <div class="nds-card-content"> <div class="nds-card-text"> <span class="nds-card-title">User Name</span> <span class="nds-card-description">Role Title</span> </div> <div class="nds-card-tags"> <span class="nds-tag nds-blue nds-sm"> <span class="nds-label" data-filter="department">Engineering</span> </span> <span class="nds-tag nds-green nds-sm"> <span class="nds-label" data-filter="role">Developer</span> </span> </div> </div> </div> </div> <nav class="nds-pagination" data-auto-pagination="basicFilterCards" aria-label="Pagination"></nav>

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.

Checkbox Filter (Multi-Select)
Task A
Active
Task B
Pending
Task C
Complete
Task D
Active
<div class="nds-toolbar"> <!-- Filter (merged with dropmenu) --> <div class="nds-dropmenu nds-filter" data-filter-target="checkboxCards" style="--dropmenu-min-width: 260px;"> <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"> <!-- Checkbox: multi-select, OR logic --> <div data-filter="status" data-filter-type="checkbox" data-filter-legend="Status" 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> <!-- Applied Filters (standalone, linked by data-filter-target) --> <div class="nds-filter-applied" data-filter-target="checkboxCards" hidden> <span class="nds-label">Applied Filters:</span> <div class="nds-chips"></div> </div> </div> <div id="checkboxCards" class="nds-grid" data-filter-items="nds-card" style="--max-col: 2; --mid-col: 2; --min-col: 1;"> <div class="nds-card nds-stroke"> <div class="nds-card-content"> <div class="nds-card-text"> <span class="nds-card-title">Task A</span> </div> <div class="nds-card-tags"> <span class="nds-tag nds-green nds-sm"> <span class="nds-label" data-filter="status">Active</span> </span> </div> </div> </div> <div class="nds-card nds-stroke"> <div class="nds-card-content"> <div class="nds-card-text"> <span class="nds-card-title">Task B</span> </div> <div class="nds-card-tags"> <span class="nds-tag nds-gray nds-sm"> <span class="nds-label" data-filter="status">Pending</span> </span> </div> </div> </div> <div class="nds-card nds-stroke"> <div class="nds-card-content"> <div class="nds-card-text"> <span class="nds-card-title">Task C</span> </div> <div class="nds-card-tags"> <span class="nds-tag nds-blue nds-sm"> <span class="nds-label" data-filter="status">Complete</span> </span> </div> </div> </div> <div class="nds-card nds-stroke"> <div class="nds-card-content"> <div class="nds-card-text"> <span class="nds-card-title">Task D</span> </div> <div class="nds-card-tags"> <span class="nds-tag nds-green nds-sm"> <span class="nds-label" data-filter="status">Active</span> </span> </div> </div> </div> </div>
Radio Filter (Single-Select)
Feature Request
High
Bug Fix
Medium
Documentation
Low
Security Patch
High
<div class="nds-toolbar"> <!-- Filter (merged with dropmenu) --> <div class="nds-dropmenu nds-filter" data-filter-target="radioCards" style="--dropmenu-min-width: 260px;"> <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"> <!-- Radio: single-select --> <div data-filter="priority" data-filter-type="radio" data-filter-legend="Priority" 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> <div class="nds-filter-applied" data-filter-target="radioCards" hidden> <span class="nds-label">Applied Filters:</span> <div class="nds-chips"></div> </div> </div> <div id="radioCards" class="nds-grid" data-filter-items="nds-card" style="--max-col: 2; --mid-col: 2; --min-col: 1;"> <div class="nds-card nds-stroke"> <div class="nds-card-content"> <div class="nds-card-text"> <span class="nds-card-title">Feature Request</span> </div> <div class="nds-card-tags"> <span class="nds-tag nds-red nds-sm"> <span class="nds-label" data-filter="priority">High</span> </span> </div> </div> </div> <div class="nds-card nds-stroke"> <div class="nds-card-content"> <div class="nds-card-text"> <span class="nds-card-title">Bug Fix</span> </div> <div class="nds-card-tags"> <span class="nds-tag nds-yellow nds-sm"> <span class="nds-label" data-filter="priority">Medium</span> </span> </div> </div> </div> <div class="nds-card nds-stroke"> <div class="nds-card-content"> <div class="nds-card-text"> <span class="nds-card-title">Documentation</span> </div> <div class="nds-card-tags"> <span class="nds-tag nds-gray nds-sm"> <span class="nds-label" data-filter="priority">Low</span> </span> </div> </div> </div> <div class="nds-card nds-stroke"> <div class="nds-card-content"> <div class="nds-card-text"> <span class="nds-card-title">Security Patch</span> </div> <div class="nds-card-tags"> <span class="nds-tag nds-red nds-sm"> <span class="nds-label" data-filter="priority">High</span> </span> </div> </div> </div> </div>
Switch Filter (Toggle)
App Alpha
SSO API
App Beta
SSO
App Gamma
API Webhooks
App Delta
Webhooks
<div class="nds-toolbar"> <!-- Filter (merged with dropmenu) --> <div class="nds-dropmenu nds-filter" data-filter-target="switchCards" style="--dropmenu-min-width: 260px;"> <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"> <!-- Switch: toggle, OR logic --> <div data-filter="feature" data-filter-type="switch" data-filter-legend="Features" 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> <div class="nds-filter-applied" data-filter-target="switchCards" hidden> <span class="nds-label">Applied Filters:</span> <div class="nds-chips"></div> </div> </div> <div id="switchCards" class="nds-grid" data-filter-items="nds-card" style="--max-col: 2; --mid-col: 2; --min-col: 1;"> <div class="nds-card nds-stroke"> <div class="nds-card-content"> <div class="nds-card-text"> <span class="nds-card-title">App Alpha</span> </div> <div class="nds-card-tags"> <span class="nds-tag nds-blue nds-sm"> <span class="nds-label" data-filter="feature">SSO</span> </span> <span class="nds-tag nds-blue nds-sm"> <span class="nds-label" data-filter="feature">API</span> </span> </div> </div> </div> <div class="nds-card nds-stroke"> <div class="nds-card-content"> <div class="nds-card-text"> <span class="nds-card-title">App Beta</span> </div> <div class="nds-card-tags"> <span class="nds-tag nds-blue nds-sm"> <span class="nds-label" data-filter="feature">SSO</span> </span> </div> </div> </div> <div class="nds-card nds-stroke"> <div class="nds-card-content"> <div class="nds-card-text"> <span class="nds-card-title">App Gamma</span> </div> <div class="nds-card-tags"> <span class="nds-tag nds-blue nds-sm"> <span class="nds-label" data-filter="feature">API</span> </span> <span class="nds-tag nds-blue nds-sm"> <span class="nds-label" data-filter="feature">Webhooks</span> </span> </div> </div> </div> <div class="nds-card nds-stroke"> <div class="nds-card-content"> <div class="nds-card-text"> <span class="nds-card-title">App Delta</span> </div> <div class="nds-card-tags"> <span class="nds-tag nds-blue nds-sm"> <span class="nds-label" data-filter="feature">Webhooks</span> </span> </div> </div> </div> </div>
Slider Filter (Range)
Wireless Hub
250
Office Chair
900
Monitor
1800
Laptop
3500
<div class="nds-toolbar"> <!-- Slider: numeric range. Both min+max = dual range; max alone = "up to" --> <div class="nds-dropmenu nds-filter" data-filter-target="sliderCards" style="--dropmenu-min-width: 300px;"> <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="price" data-filter-type="slider" data-filter-legend="Price" data-filter-min="0" data-filter-max="5000" data-filter-step="100" data-filter-currency="SAR" 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> <div class="nds-filter-applied" data-filter-target="sliderCards" hidden> <span class="nds-label">Applied Filters:</span> <div class="nds-chips"></div> </div> </div> <!-- Each card carries a numeric data-filter-value --> <div id="sliderCards" class="nds-grid" data-filter-items="nds-card"> <div class="nds-card nds-stroke"> <div class="nds-card-content"> <div class="nds-card-text"><span class="nds-card-title">Wireless Hub</span></div> <div class="nds-card-tags"> <span class="nds-tag nds-blue nds-sm"><span class="nds-label nds-number-format" data-currency="SAR" data-filter="price" data-filter-value="250">250</span></span> </div> </div> </div> <!-- ...more cards... --> </div>

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.

Department Inline, Role and Status Collapsible
Ahmed Al-Rashidi Senior Developer
Engineering Senior Developer active
Fatima Al-Harbi UX Designer
Design UX Designer active
Sara Al-Dosari Marketing Lead
Marketing Marketing Lead active
Layla Al-Qahtani HR Specialist
Human Resources HR Specialist active
Khalid Al-Otaibi Project Manager
Engineering Project Manager active
Omar Al-Shahrani Data Analyst
Finance Data Analyst active
Mohammed Al-Zahrani Security Engineer
Engineering Security Engineer away
Noura Al-Ghamdi Content Strategist
Marketing Content Strategist active
Abdulaziz Al-Shehri Finance Manager
Finance Finance Manager active
Hana Al-Mutairi Frontend Developer
Engineering Frontend Developer offline
Turki Al-Subaie Operations Lead
Human Resources Operations Lead active
Reem Al-Tamimi Visual Designer
Design Visual Designer active
<div class="nds-toolbar"> <div class="nds-dropmenu nds-filter" data-filter-target="accordionFilterCards"> <button class="nds-btn nds-neutral nds-menu-btn 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"> <!-- Short group: stays inline --> <div data-filter="department" data-filter-type="checkbox" data-filter-legend="Department" data-no-auto-close> </div> <hr class="nds-divider"> <!-- Long groups: collapsible, with a count tag on the header --> <div data-filter="role" data-filter-type="checkbox" data-filter-legend="Role" data-filter-accordion data-no-auto-close> </div> <hr class="nds-divider"> <div data-filter="status" data-filter-type="radio" data-filter-legend="Status" data-filter-accordion 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">Filter</span> </button> </div> </div> </div> </div> <div class="nds-filter-applied" data-filter-target="accordionFilterCards" hidden> <span class="nds-label">Applied Filters:</span> <div class="nds-chips"></div> </div> </div> <!-- Filterable Cards --> <div id="accordionFilterCards" class="nds-paged-content nds-grid" data-filter-items="nds-card" style="--per-page: 6; --max-col: 3; --mid-col: 2; --min-col: 1;"> <div class="nds-card nds-stroke nds-page-item"> <div class="nds-card-content"> <div class="nds-card-text"> <span class="nds-card-title">User Name</span> <span class="nds-card-description">Role Title</span> </div> <div class="nds-card-tags"> <span class="nds-tag nds-blue nds-sm"> <span class="nds-label" data-filter="department">Engineering</span> </span> <span class="nds-tag nds-green nds-sm"> <span class="nds-label" data-filter="role">Developer</span> </span> <span class="nds-tag nds-neutral nds-sm"> <span class="nds-label" data-filter="status">active</span> </span> </div> </div> </div> </div> <nav class="nds-pagination" data-auto-pagination="accordionFilterCards" aria-label="Pagination"></nav>

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.

Search + Filter + Slider
Suggestions:
USB-C Hub
Accessories250
Wireless Mouse
Accessories120
Wireless Keyboard
Accessories480
27" Monitor
Displays950
Ergonomic Chair
Furniture1500
Standing Desk
Furniture2800
Laptop Pro
Computers4200
Headphones
Accessories600
<!-- One flex bar above the grid; each surface is wired by its own data-filter-target --> <div class="nds-toolbar"> <!-- Search box (field + button) --> <div class="nds-form-container nds-search-box" data-filter-target="barFilterCards"> <div class="nds-search-content"> <div class="nds-form-control"> <i class="nds-icon nds-hgi-search-01" aria-hidden="true"></i> <input id="barSearchInput" type="text" class="nds-search-input" name="search" placeholder="Search products..."> <div class="nds-form-action"> <button class="nds-btn nds-subtle nds-clear" hidden aria-label="Clear search"> <i class="nds-icon nds-hgi-cancel-01" aria-hidden="true"></i> </button> </div> </div> <button class="nds-btn nds-primary nds-search-btn" type="button"> <i class="nds-icon nds-hgi-search-01" aria-hidden="true"></i> <span class="nds-label" data-hidden="sm sr">Search</span> </button> </div> </div> <!-- Filter: category checkbox + price slider --> <div class="nds-dropmenu nds-filter" data-filter-target="barFilterCards" style="--dropmenu-min-width: 300px;"> <button class="nds-btn nds-neutral nds-menu-btn 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="category" data-filter-type="checkbox" data-filter-legend="Category" data-no-auto-close></div> <hr class="nds-divider"> <!-- Slider filter: data-filter-max alone = single "up to"; both bounds = dual range --> <div data-filter="price" data-filter-type="slider" data-filter-legend="Price" data-filter-min="0" data-filter-max="5000" data-filter-step="100" data-filter-currency="SAR" 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> <!-- Applied-filter chips (filled by JS) --> <div class="nds-filter-applied" data-filter-target="barFilterCards" hidden> <span class="nds-label">Applied Filters:</span> <div class="nds-chips"></div> </div> <!-- Auto-fill suggestions; data-autofill-apply runs the search on click --> <div class="nds-auto-fill" data-target="barSearchInput" data-filter-target="barFilterCards" data-autofill-apply> <span class="nds-label">Suggestions:</span> <div class="nds-chips"> <button type="button" class="nds-chip nds-neutral nds-rounded nds-item"> <i class="nds-icon nds-hgi-plus-sign" aria-hidden="true"></i><span class="nds-label">Furniture</span> </button> <!-- ...more suggestion chips... --> </div> </div> </div> <!-- Items: each card carries a numeric data-filter-value + data-sort-* --> <div id="barFilterCards" class="nds-paged-content nds-grid" data-filter-items="nds-card" style="--per-page: 6;"> <div class="nds-card nds-stroke nds-page-item" data-sort-name="USB-C Hub" data-sort-price="250"> <div class="nds-card-content"> <div class="nds-card-text"><span class="nds-card-title">USB-C Hub</span></div> <div class="nds-card-tags"> <span class="nds-tag nds-green nds-sm"><span class="nds-label" data-filter="category">Accessories</span></span> <span class="nds-tag nds-blue nds-sm"><span class="nds-label nds-number-format" data-currency="SAR" data-filter="price" data-filter-value="250">250</span></span> </div> </div> </div> <!-- ...more cards... --> </div> <nav class="nds-pagination" data-auto-pagination="barFilterCards" aria-label="Pagination"></nav>

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.

Explicit Values (Radio)
AI Research
Technology
Brand Identity
Design
Market Analysis
Business
Cloud Migration
Technology
UX Audit
Design
Revenue Report
Business
<div class="nds-toolbar"> <!-- Object form: keys = machine values, values = display labels --> <div class="nds-dropmenu nds-filter" data-filter-target="staticCards"> <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="category" data-filter-type="radio" data-filter-legend="Category" data-filter-values='{"tech":"Technology","design":"Design","biz":"Business"}' 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">Filter</span> </button> </div> </div> </div> </div> <div class="nds-filter-applied" data-filter-target="staticCards" hidden> <span class="nds-label">Applied Filters:</span> <div class="nds-chips"></div> </div> </div> <!-- Cards use data-filter-value to match machine keys --> <div id="staticCards" class="nds-grid" data-filter-items="nds-card"> <div class="nds-card nds-stroke"> <div class="nds-card-content"> <div class="nds-card-tags"> <span class="nds-tag nds-blue nds-sm"> <span class="nds-label" data-filter="category" data-filter-value="tech">Technology</span> </span> </div> </div> </div> </div>

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.

Populate from API

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>
Cascading Filters

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.

AJAX Filter Form

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.

<!-- Submission form: separate element linked via data-filter-target --> <form id="resultsForm" data-filter-target="results" data-filter-submit data-ajax method="GET" action="https://api.example.com/search"> <!-- Search Box (linked by data-filter-target) --> <div class="nds-form-container nds-search-box" data-filter-target="results"> <div class="nds-search-content"> <div class="nds-form-control"> <i class="nds-icon nds-hgi-search-01" aria-hidden="true"></i> <input type="text" class="nds-search-input" name="search" placeholder="Search..."> </div> <button class="nds-btn nds-primary nds-search-btn" type="submit"> <span class="nds-label" data-hidden="sm sr">Search</span> </button> <!-- Filter anchor (pure marker, not the form) --> <div class="nds-dropmenu nds-filter" data-filter-target="results"> <button class="nds-btn nds-neutral nds-dropmenu-trigger" type="button"> <i class="hgi hgi-stroke hgi-filter"></i> <span class="nds-label">Filter</span> </button> <div class="nds-dropmenu-menu" hidden> <div class="nds-dropmenu-scroll"> <div data-filter="category" data-filter-type="checkbox" data-filter-legend="Category" data-filter-values='{"news":"News","services":"Services","events":"Events"}' 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" data-filter-action="apply"> <span class="nds-label">Apply</span> </button> </div> </div> </div> </div> </div> <div class="nds-filter-applied" data-filter-target="results" hidden> <span class="nds-label">Applied Filters:</span> <div class="nds-chips"></div> </div> </div> </form> <div id="results"> <!-- HTML response will be injected here --> </div>
Custom AJAX Rendering (preventDefault)

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

Auto-initialization

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.

Auto-Generated Filters

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.

Searchable Options

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.

Slider Filtering

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.

Collapsible Groups

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.

Shareable URL State

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.

Applied Filter Chips

Active filters display as removable chips below the filter bar. Clicking a chip removes that filter and re-applies the remaining criteria.

No Results Alert

Shows a warning alert with a "Clear Filter" action when no cards match the current criteria. The alert dismisses automatically when results reappear.

Dynamic and Cascading Filters

Use populateFilter() to generate or replace filter inputs at runtime. Supports cascading filters where one selection drives another filter's options via API.

AJAX Form Submission

Supports server-side filtering via AJAX with automatic HTML response injection and JSON response events for custom rendering.

Custom Item Selectors

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.

Value and Label Mapping

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.

Deferred Option Build

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.

Programmatic Control

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. Use data-filter-values to supply explicit values when cards don't exist or values differ from card content. Use populateFilter() 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-max alone gives an "up to" thumb. Give each card a numeric data-filter-value on its data-filter marker; the visible text can still read SAR 250 while 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-applied container 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-legend value appears as the fieldset heading inside the dropmenu
  • Add data-filter-accordion to 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

ClassDescription
nds-filterThe filter anchor. Add it next to nds-dropmenu for the standard Filter button and menu.
nds-filter-btnMarks the always-visible trigger button. It carries the applied-filter count badge and the loading spinner during a submission.
nds-filter-appliedThe applied-chips row. Give it an inner .nds-chips element and the filter fills it.
nds-auto-fillA suggestion row shown only while no filter is applied. Same label plus chips layout as the applied row.
nds-filter-menuAdded 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-rangeThe fieldset a slider filter generates. Read-only hook for styling.

Data Attributes

Filter Anchor (.nds-filter)

AttributeDescription
data-filter-targetID 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-searchWhen 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-portalMoves 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>)

AttributeDescription
data-filter-targetMust match the anchor's target id to activate form mode for that filter instance.
data-filter-submitMarks this form as the submission form (enables form mode instead of client-side filtering).
data-ajaxUse AJAX instead of page navigation (requires data-filter-submit).

Search Input Opt-Out

AttributeDescription
data-filter-ignorePlace 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

AttributeDescription
data-filter-itemsSet 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-countSet 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

AttributeDescription
data-filter-countPlace 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-queryPlace 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

AttributeDescription
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-typeAuto-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-maxSlider 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-stepSlider only. Snap increment for the thumb(s). Default: 1.
data-filter-currencySlider only. Currency code (e.g. SAR) shown on the slider value outputs and the applied-filter chip via number formatting.
data-filter-unitSlider 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-labelOverride the auto-prepended "All" label on radio groups. Default: الكل in Arabic, All otherwise.
data-filter-no-allOpt out of the auto-prepended "All" option on radio groups (boolean attribute).
data-filter-valuesJSON 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-legendFieldset legend text for auto-generated filter groups
data-filter-accordionBoolean 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-variantCSS class to add to auto-generated input elements (e.g. nds-primary)
data-filter-valueSet 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

AttributeDescription
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

AttributeDescription
data-chip-classSet on .nds-filter-applied to customize chip styling. Default: nds-primary nds-lg

Auto-Fill Container

Class / AttributeDescription
.nds-auto-fillPlace 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.

AttributeDescription
data-urlAPI endpoint that returns the suggestions.
data-nameJSON field to display from each result. Default: Title.
data-query-paramQuery parameter name for the typed term. Default: q.

CSS Custom Properties

PropertyDefaultDescription
--dropmenu-min-width250pxMinimum 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.

SelectorEffectWhen set
.nds-filter[data-state~="submitting"]pointer-events: noneSet 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

  • Enter in the page search box runs the search straight away
  • Enter anywhere 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.

// ── Get a filter instance ─────────────────────────── const filter = NDS.Filter.getInstance('.nds-filter'); const filter = NDS.Filter.getInstance(element); const filter = NDS.Filter.getByTarget('cardList'); // ── Wait for initialization (safe with deferred scripts) ── NDS.Filter.whenReady('.nds-filter', (instance) => { // instance is guaranteed ready // fires immediately if already initialized }); // ── Set filters and search programmatically ───────── filter.setFilterValues('department', ['Engineering', 'Design']); // Check/uncheck existing inputs filter.setSearchValue('Ahmed'); filter.removeFilterValue('department', 'Design'); filter.removeSearchFilter(); // Clear the search keyword, keep the other filters filter.resetRangeFilter('price'); // Put a slider filter back to its full span // ── Generate filter inputs from values (no card scanning) ── filter.populateFilter('system', ['Identity', 'Transport', 'Healthcare']); // checkbox (default) filter.populateFilter('priority', ['High', 'Medium', 'Low'], 'radio'); // radio filter.populateFilter('system', ['New A', 'New B']); // re-calling replaces previous inputs // Note: populateFilter() owns its values — refresh() will not overwrite them // ── Query current state ───────────────────────────── const criteria = filter.getCriteria(); // Returns: { search: 'ahmed', filters: { department: ['Engineering'] } } const visible = filter.getVisibleItems(); // Array of visible card elements const hidden = filter.getHiddenItems(); // Array of hidden card elements // ── Reset and refresh ─────────────────────────────── filter.reset(); // Clear all filters and search, show all items filter.clear(); // Clear all inputs without re-showing items filter.refresh(); // Re-resolve target container, re-scan items, regenerate auto filters // ── Manual control ────────────────────────────────── filter.applyFilters(); // Trigger filtering logic manually filter.submitForm(); // Submit the form (form submission mode only) filter.destroy(); // Show all items and remove initialization flag // ── No-results alert ──────────────────────────────── // Client-side mode raises and dismisses this automatically. Form and AJAX modes // don't — the server owns the result set, so call these off your own response. // Both need data-filter-target: without it there is no container to render into // and the call is a silent no-op. filter.showNoResultsAlert(); // Warning alert + a Clear Filter button wired to reset() filter.dismissNoResultsAlert(); // Remove it when results come back // Re-apply URL params after dynamically adding filter inputs filter.reapplyUrlParamsForFilter('system'); // ── Static methods ────────────────────────────────── NDS.Filter.init(); // Initialize any new .nds-filter elements on page NDS.Filter.reinit(); // Same as init() NDS.Filter.create(containerEl); // Manually instantiate — registers like init() does; // re-creating on the same element returns the existing instance // ── Events ────────────────────────────────────────── // nds:filter:ready - Filter initialized filterEl.addEventListener('nds:filter:ready', (e) => { const instance = e.detail; // the NDSFilter instance }); // nds:filter:change - Fires after every applyFilters() call, including when // all criteria are cleared (visibleItems === totalItems in that case) filterEl.addEventListener('nds:filter:change', (e) => { const { criteria, totalItems, visibleItems, hiddenItems } = e.detail; }); // nds:filter:reset - All filters cleared via reset() filterEl.addEventListener('nds:filter:reset', (e) => { const { totalItems } = e.detail; }); // nds:filter:clear - Dropmenu clear button clicked filterEl.addEventListener('nds:filter:clear', (e) => { const { filter } = e.detail; }); // ── Form submission events (standard + AJAX mode) ─── // nds:formInvalid - Standard form submission failed validation filterEl.addEventListener('nds:formInvalid', (e) => { const { invalidFields, errors } = e.detail; }); // nds:formValid - Standard form passed validation, navigation will proceed filterEl.addEventListener('nds:formValid', (e) => { // e.detail is {} }); // ── AJAX mode events ──────────────────────────────── // nds:filterFormSubmit - Before any form submission (cancelable) filterEl.addEventListener('nds:filterFormSubmit', (e) => { const { criteria, form } = e.detail; }); // nds:filterFormAjax - Before AJAX request (cancelable) // Call e.preventDefault() to handle the request yourself. // rollback() restores chips, badge, controls and URL params to their // pre-submission values — call it if your own request fails, so they don't // describe results that were never rendered. filterEl.addEventListener('nds:filterFormAjax', (e) => { const { criteria, form, hiddenInputsContainer, rollback } = e.detail; }); // nds:filterFormComplete - AJAX response received filterEl.addEventListener('nds:filterFormComplete', (e) => { const { success, isJson, data, html, form } = e.detail; }); // nds:filterFormError - AJAX request failed (cancelable) // Fires on a network error, a non-OK status, a timeout, or a response that // carries no #target element. The target container is never modified on // failure — whatever was on screen stays — and the applied state (chips, // badge, controls, URL params) is rolled back to match it, so a failed Clear // keeps the filters it was about to drop. Filter raises an error toast after // this event; preventDefault() suppresses it if you show your own. filterEl.addEventListener('nds:filterFormError', (e) => { const { error, form } = e.detail; e.preventDefault(); // optional — skip the built-in toast });
Last Modified Date: 06/08/2026 - 03:36 AM
Was this page useful?
60% of users said Yes from 2843 Feedbacks