Filters

This page describes an agreed design that is not built yet. What ships today is a row of separate controls above the list.

It applies to any list a reader narrows. Examples below are written as CONDITION · VALUE rather than with real field names, because the rules hold whatever the list holds.

Every condition lives in one widget

A control that narrows the list belongs inside the filter widget. A row of controls above the list grows sideways with every condition added to it, and reading what is currently set means scanning the row from one end to the other.

Collected in one place, the conditions can be read at a glance. The widget sits beside the list on wide screens and inside an overlay on narrow ones, with the same contents in both.

A collapsed section says what it is holding

Each condition is one section, and a section collapses. More than a handful open at once are taller than the screen, so a reader opens the one they came to change.

A collapsed header names the section's current selection. Collapsing hides the values, and a hidden condition looks exactly like one that was never set. The header carries that difference.

Section stateHeader
Nothing setCONDITION
One valueCONDITION · VALUE
Several valuesCONDITION · VALUE +2
A value longer than the headerCONDITION · a-long-value…
Values that do not read as one setCONDITION · 3 selected

The last row is for a section holding values of more than one kind, such as a set of states alongside a list of names. Naming one of them in the header implies the others are the same kind, so the header carries a count instead.

A section whose controls combine into a single condition states the result. Where a toggle and a range together produce one phrase, the header carries the phrase rather than listing the two controls behind it.

Choosing a control for a new condition

A condition that picks its own control makes the reader relearn the widget one section at a time. Read the table from the top and take the first row that fits.

The conditionThe control
Is on or off, and off means no conditionToggle
Changes which options appear below itSegmented control
Takes one value from a fixed set of four or fewerRadio
Takes one value from a long or growing setDropdown
Takes several valuesCheckbox list
Takes a rangeA purpose-built input — a calendar, for dates

A toggle is off by default, and switching it on narrows the list. A toggle that narrows when off is a condition nobody set and nobody can see, and the reader has no way to account for the missing rows.

An empty checkbox list means everything. Do not add an "All" checkbox: it gives the widget two ways to say the same thing, and they disagree as soon as one value is unchecked.

Show five values and load the rest on demand when the set of options comes from the account rather than from us. Those arrive in unknown numbers, and one long list pushes every section below it off the screen.

Values within a section are OR, and sections are AND. A condition that needs different logic gets its own section rather than a control that behaves unlike its neighbours.

Don't take multiple values through chips or a multi-select

Chips, tag inputs and multi-selects show what is chosen and hide what could have been. In a filter the unchosen values are information too — knowing that three more options exist is what makes an empty result trustworthy. Use a checkbox list.

Some conditions are too complex for the pattern

A condition that cannot be stated as a list of values does not get a list of values. A time range is the clearest case: it combines a relative range, an absolute range and a point in the deploy history, and choosing one of them replaces the other two. No checkbox list expresses that.

Give such a condition its own section and a control built for it, and let that control follow its own pattern rather than its neighbours'. The reader then has one more thing to learn, so the test is whether the condition genuinely cannot be a list. Most conditions can.

Time scoping has a page of its own, Date and time filter, describing a dropdown built from a time range, a deploy revision and a custom range. Both designs are ahead of the code, and which one a list ends up with is not settled.

Sections that take one value, and sections that take several

A section takes one value when its values are mutually exclusive. Nothing in it is a checkbox. A control that replaces the options below it belongs here, which is what a segmented control is for.

Every other section is a checkbox list. Where the options are a long or growing set, the list follows the five-at-a-time rule above.

Two further rules:

The widget and the search bar hold one state

The text in the search bar is the widget's state written out. Changing either one updates the other.

A filter built by clicking therefore ends up as one line of text, which can be pasted to a colleague and kept in a URL. Text that arrives that way opens as structure. Nobody has to learn the query syntax and the widget as two separate things.

Neither half gets a condition the other cannot express

Anything the widget can set has to be writable as text, and anything writable as text has to appear in the widget. A condition that exists on one side only leaves no way to tell which side is the real state.

The widget stays put while the list empties

A combination that matches nothing leaves the filter widget where it is and puts the empty state in the list. Undoing a filter needs the filter still on screen.

The empty state itself follows Empty state.

Below lg the filters open over the page

There is no room beside the list for the widget at those widths, so a button next to the search bar opens the same contents as an overlay. The button counts the conditions currently set, so a filtered list is recognisable without opening anything.

The overlay follows Responsive navigation: a scrim behind it, a close control, and one overlay at a time.

The overlay has one control the wide layout does not — Done, at the bottom. On a wide screen the list reacts underneath as each value is ticked, and the overlay covers the list it is filtering. Closing it is how the reader sees the result, so the close sits where the reading ends.