Forms and controls
Filter
The filter panel above a catalog: one row of selects on wide screens, a Filters disclosure on phones, one GET form.
- Block
.hs-filter- CSS
components/filter.css(in/brand.css)- JS
components/filter.js: load it only on a page with this component- Status
- Decided
- Decided in
- Directories round
- Markdown
- /components/filter.md
What it is
A GET form that narrows a catalog by two to four fixed dimensions. The fields sit in a details panel. Below 48rem the panel is a closed "Filters" disclosure with the active count. From 48rem filter.js opens it, CSS hides the summary, and the selects sit in one row above the results. A select submits the form on change; the Apply button stays for the no-JS page.
When to use
- A catalog with two or more dimensions: the scholarship directory (who it is for, type, offered by), the benefits list (category, membership needed).
- Any list long enough to need a pager.
When not to use
- One dimension over a short list: use Filter chips, as the article topics.
- A keyword alone: use Search.
- Sibling pages at their own URLs: use Tabs.
Rules
- Markup:
form.hs-filter[method="get"]>details.hs-filter__panel[data-hs-filter]>summary.hs-filter__summary+div.hs-filter__fields. - The summary reads
Filters, thenspan.hs-filter__countwith the number of active filters. Leave the count out when no filter is set. - Each field is
div.hs-field.hs-filter__fieldwithlabel.hs-labelandselect.hs-control. The first option is the empty value: "Any type", "Anyone". - The last item in the fields is
div.hs-filter__actions:button.hs-button.hs-button--navy.hs-button--sm"Apply filters", thena.hs-form-link.hs-filter__clear"Clear all" to the clean URL. Print Clear all only when a filter is set. It shows only while the panel is a drawer (under 48rem, or withoutfilter.js):filter.jsaddsis-inlineto the open panel on wide screens andfilter.csshides the link there, because the Results bar carries Clear all. - Two to four selects. A dimension with more than about 12 values needs a curated map in code, not every tag.
- Option labels are visitor words ("Graduate", "Study abroad"), never internal tags or school names.
- The form keeps the keyword (
q) as a hidden field, so a filter change does not drop the search. - Filtering runs on the server: the URL holds the state and every option is a real link target. A short list (about 40 items) may filter in the page on the same markup.
- Load
filter.json the page. The sort select in the Results bar joins this form withform="FORM-ID".
Accessibility
- Each select has a visible
label for. The summary is the native disclosure button, 48px tall. - The count is part of the summary text, so a screen reader hears "Filters 2".
- Without
filter.jsthe summary stays visible at every width, so the panel always opens; the Apply button submits. - Before an auto-submit,
filter.jsdisables every empty field of the form and of its joined controls, so the URL holds set filters only (?for=undergraduate&sort=name). The no-JS Apply button still submits all fields. - Auto-submit on change reloads the page with the new list; the Results bar count, a
role="status"line, states the new total. - The chevron turns with a 200ms transition; reduced motion removes it.
- Summary navy 12.4:1, count white on navy 12.4:1, labels ink 13.0:1 on white.
Variants
Filter panel
Narrow the window under 768px to see the closed disclosure. Needs filter.js.
Preview on soft
<form class="hs-filter" id="catalog-filter" method="get" action="/scholarship-directory">
<details class="hs-filter__panel" data-hs-filter>
<summary class="hs-filter__summary">Filters</summary>
<div class="hs-filter__fields">
<div class="hs-field hs-filter__field">
<label class="hs-label" for="f-for">Who it’s for</label>
<select class="hs-control" id="f-for" name="for">
<option value="">Anyone</option>
<option value="undergraduate">Undergraduate</option>
<option value="graduate">Graduate</option>
<option value="alumni">Alumni</option>
<option value="international">International</option>
<option value="multicultural">Multicultural</option>
</select>
</div>
<div class="hs-field hs-filter__field">
<label class="hs-label" for="f-type">Type</label>
<select class="hs-control" id="f-type" name="type">
<option value="">Any type</option>
<option value="scholarship">Scholarship</option>
<option value="grant">Grant</option>
<option value="fellowship">Fellowship</option>
<option value="internship">Internship</option>
<option value="study-abroad">Study abroad</option>
</select>
</div>
<div class="hs-field hs-filter__field">
<label class="hs-label" for="f-by">Offered by</label>
<select class="hs-control" id="f-by" name="by">
<option value="">Any provider</option>
<option value="honor-society">Honor Society®</option>
<option value="other">Other providers</option>
</select>
</div>
<div class="hs-filter__actions">
<button class="hs-button hs-button--navy hs-button--sm" type="submit">Apply filters</button>
</div>
</div>
</details>
</form>With active filters
Two filters set: the summary shows the count, the selects keep their values, and Clear all returns to the clean URL.
Preview on soft
<form class="hs-filter" id="catalog-filter-2" method="get" action="/scholarship-directory">
<input type="hidden" name="q" value="leadership">
<details class="hs-filter__panel" data-hs-filter>
<summary class="hs-filter__summary">Filters <span class="hs-filter__count">2</span></summary>
<div class="hs-filter__fields">
<div class="hs-field hs-filter__field">
<label class="hs-label" for="f2-for">Who it’s for</label>
<select class="hs-control" id="f2-for" name="for">
<option value="">Anyone</option>
<option value="undergraduate">Undergraduate</option>
<option value="graduate" selected>Graduate</option>
</select>
</div>
<div class="hs-field hs-filter__field">
<label class="hs-label" for="f2-type">Type</label>
<select class="hs-control" id="f2-type" name="type">
<option value="">Any type</option>
<option value="scholarship">Scholarship</option>
<option value="fellowship" selected>Fellowship</option>
</select>
</div>
<div class="hs-field hs-filter__field">
<label class="hs-label" for="f2-by">Offered by</label>
<select class="hs-control" id="f2-by" name="by">
<option value="">Any provider</option>
<option value="honor-society">Honor Society®</option>
<option value="other">Other providers</option>
</select>
</div>
<div class="hs-filter__actions">
<button class="hs-button hs-button--navy hs-button--sm" type="submit">Apply filters</button>
<a class="hs-form-link hs-filter__clear" href="/scholarship-directory">Clear all</a>
</div>
</div>
</details>
</form>