90Filter rail
The course listing's rail: facet groups as native <details>, a dual-thumb price range built from two real <input type="range">, and the applied set echoed as removable chips above the grid — a filter you cannot see is a filter you forget you set. Everything is a real form control, so the page filters with JavaScript off and the form submits.
Use it for
- The course listing, as a sticky rail ≥ lg and a drawer below it
- Any catalog with more than about a dozen items
Not for
- Fewer than three facets — that is a row of filter chips, not a rail
- Search. A filter narrows a set; search queries it
The rail
Live — check a facet to see the applied chips appear, drag either price thumb, or clear everything.
51 courses
// CourseApex basics
// CourseSOQL, properly
markup
<div data-filters style="display:grid;grid-template-columns:16rem minmax(0,1fr);gap:var(--space-8);inline-size:100%;align-items:start">
<form class="ns-filters">
<div class="ns-filters__head">
<span class="ns-filters__title">Filters</span>
<button type="button" class="ns-btn ns-btn--quiet ns-btn--xs ns-filters__clear" data-clear-filters hidden>Clear all</button>
</div>
<details class="ns-filters__group" open>
<summary class="ns-filters__legend">Level <i class="ph ph-caret-down" aria-hidden="true"></i></summary>
<div class="ns-filters__list">
<label class="ns-choice"><input class="ns-checkbox" type="checkbox" name="level" value="beginner"><span>Beginner</span><span class="ns-filters__count">24</span></label>
<label class="ns-choice"><input class="ns-checkbox" type="checkbox" name="level" value="intermediate"><span>Intermediate</span><span class="ns-filters__count">18</span></label>
<label class="ns-choice"><input class="ns-checkbox" type="checkbox" name="level" value="advanced"><span>Advanced</span><span class="ns-filters__count">9</span></label>
</div>
</details>
<details class="ns-filters__group" open>
<summary class="ns-filters__legend">Price <i class="ph ph-caret-down" aria-hidden="true"></i></summary>
<div class="ns-range">
<div class="ns-range__track">
<span class="ns-range__fill"></span>
<input type="range" data-range="from" min="0" max="200" value="0" step="5" aria-label="Minimum price">
<input type="range" data-range="to" min="0" max="200" value="120" step="5" aria-label="Maximum price">
</div>
<div class="ns-range__values">
<input class="ns-input" type="number" data-range-out value="0" min="0" max="200" aria-label="Minimum price, exact">
<span class="ns-range__sep">—</span>
<input class="ns-input" type="number" data-range-out value="120" min="0" max="200" aria-label="Maximum price, exact">
</div>
</div>
</details>
<details class="ns-filters__group" open>
<summary class="ns-filters__legend">Lesson type <i class="ph ph-caret-down" aria-hidden="true"></i></summary>
<div class="ns-filters__list">
<label class="ns-choice"><input class="ns-checkbox" type="checkbox" name="type" value="video"><span>Video</span><span class="ns-filters__count">31</span></label>
<label class="ns-choice"><input class="ns-checkbox" type="checkbox" name="type" value="lab"><span>Hands-on labs</span><span class="ns-filters__count">14</span></label>
<label class="ns-choice"><input class="ns-checkbox" type="checkbox" name="type" value="live"><span>Live sessions</span><span class="ns-filters__count">3</span></label>
</div>
</details>
<details class="ns-filters__group">
<summary class="ns-filters__legend">Duration <i class="ph ph-caret-down" aria-hidden="true"></i></summary>
<div class="ns-filters__list">
<label class="ns-choice"><input class="ns-checkbox" type="checkbox" name="len" value="short"><span>Under 2 hours</span><span class="ns-filters__count">12</span></label>
<label class="ns-choice"><input class="ns-checkbox" type="checkbox" name="len" value="mid"><span>2 – 6 hours</span><span class="ns-filters__count">26</span></label>
<label class="ns-choice"><input class="ns-checkbox" type="checkbox" name="len" value="long"><span>Over 6 hours</span><span class="ns-filters__count">13</span></label>
</div>
</details>
</form>
<div>
<div class="ns-filters__applied" data-applied hidden></div>
<div class="ns-course-filters">
<span class="ns-filters__title">51 courses</span>
<select class="ns-select" aria-label="Sort by"><option>Most popular</option><option>Newest</option><option>Highest rated</option></select>
</div>
<div class="ns-course-grid">
<div class="ns-card ns-ccard ns-ccard--compact"><div class="ns-card__body"><span class="ns-card__kicker">// Course</span><a class="ns-card__link" href="#"><span class="ns-card__title">Apex basics</span></a><span class="ns-ccard__meta"><span>12 lessons</span></span></div></div>
<div class="ns-card ns-ccard ns-ccard--compact"><div class="ns-card__body"><span class="ns-card__kicker">// Course</span><a class="ns-card__link" href="#"><span class="ns-card__title">SOQL, properly</span></a><span class="ns-ccard__meta"><span>6 lessons</span></span></div></div>
</div>
</div>
</div>Accessibility contract
- The price range is TWO named sliders, not one custom widget: keyboard, screen reader and mobile all work because the controls are native
- Dragging clamps rather than swaps — swapping makes the thumb jump out from under the pointer
- Each chip's accessible name is "Remove filter: name"; removing one unchecks its checkbox and fires a real change event
- Counts sit inside the label, so they are announced with the option they belong to