Filter
Filtering is the only component in this system whose entire job is to hide clinical information. That makes it the one that most needs to be honest: what is hidden, how much of it, and how to get it back — visible at all times, in one glance, without opening anything.
Overview
A clinician who forgets a filter is on is looking at a subset of a patient's record while believing they are looking at the record. Every rule here follows from that single failure: filters are loud, counted and reversible.
The system's default position is that filters are a convenience for large sets, not a workflow. They never persist silently, never survive a patient change, and never apply to anything at advisory priority or above.
Always visible
Active filters render as tags above the data they act on. Never a count in a button, never a coloured icon, never only inside a panel.
Always counted
4 of 19. The hidden quantity is stated as a number, next to the data, at all
times.
Never hides an alarm
No filter, of any kind, can remove an unacknowledged alarm at advisory priority or above from view.
Anatomy
Showing 4 of 19 patients · 15 hidden by filters
| Part | Rule |
|---|---|
| Add filter | A single entry point. Opens a menu of dimensions, not a wall of always-visible controls that consumes the screen the data needs. |
| Filter tag | One per active criterion, reading dimension: value in full. Removable individually with a labelled control. |
| Count line | Required. n of N plus the number hidden, adjacent to the
data, always rendered — not only when filtering is active. |
| Clear all | States how many it will clear. One action returns the complete set. |
| Escape hatch | Anything at advisory priority or above that the filter would have hidden is surfaced above the set, with a route to it. |
What a filter may never hide
An unacknowledged alarm at advisory priority or above is not filterable. If a patient matching no active filter has one, the set reports it above the fold with a route to the patient. The clinician chose to narrow their view; they did not choose to stop being told about deterioration, and no filter UI is allowed to interpret it that way.
This is a governed behaviour. It has no configuration flag, no "advanced" setting, and no per-deployment override.
- Not filterable: unacknowledged critical, urgent and advisory alarms; a patient's identity band; any item the clinician has explicitly flagged.
- Filterable: resolved and acknowledged items, informational status, ward and team scoping, time windows, result types.
- Never filter to zero without saying why. An empty filtered set names the filters responsible and offers to remove them — see Empty state.
- Never filter across a patient boundary. Filters are cleared on patient change and never restored from a previous patient's session.
Persistence
A filter that outlives the clinician's memory of setting it is the hazard. So persistence is deliberately weak, and always announced.
| Boundary | Behaviour |
|---|---|
| Within a view, same session | Filters persist. |
| Navigating away and back | Filters persist and the tag row is re-announced on return. |
| Patient change | Cleared. Without exception. |
| New session or new user | Cleared. Filters are never part of a saved user profile that applies silently at sign-in. |
| Shift handover | Cleared, and the handover view is never filtered — see Triage worklist. |
| Saved views | Permitted, but a saved view is applied only by explicit selection and names itself in the tag row as a saved view. |
States
| State | Rendering |
|---|---|
| None active | Tag row absent; the count line still renders — 19 of 19. The absence of
filtering is information too. |
| Active | Tags visible, count line shows the hidden quantity, Clear-all states its count. |
| Applying | The previous set stays on screen with an activity indicator. Filtering never blanks the region it is narrowing. |
| Zero results | Empty state naming the responsible filters, with a one-action route back. |
| Filter unavailable | The dimension is offered but marked unavailable with a reason. A dimension that silently disappears makes the clinician doubt the whole control. |
| Escaped item present | The above-the-fold alarm callout, rendered whether or not the set is empty. |
Do's and don'ts
Each criterion legible and separately removable, sitting directly above the data it narrows.
Two criteria hidden behind a number. The clinician can see that something is filtered, which is exactly the state that gets misremembered as nothing.
Showing 4 of 19 patients · 15 hidden by filters
The hidden quantity as a number. Fifteen is a size the reader can weigh; "filtered" is not.
4 patients
A count with no denominator. Indistinguishable from a ward with four patients in it.
The filter narrowed the view, not the duty of care. The alarm escapes and says that it did.
Showing 4 of 19 patients
Correct, complete, and still wrong: one of the fifteen hidden patients has an unacknowledged critical alarm and nothing on screen says so.
The criterion in the clinician's own words, complete enough to check against what they meant.
A field name and a raw value. Unreadable under time pressure, ambiguous about time zone, and unlabelled for assistive technology.
Accessibility
- The tag row is a labelled list —
role="list"witharia-label="Active filters"— so the whole filter state can be read as a set, with its size announced. - Each remove control names its filter in full: "Remove filter Ward: Emergency". A row of buttons all called "Remove" is unusable non-visually.
- The count line is a live region (
aria-live="polite") so applying a filter announces the new denominator rather than silently changing it. - Clear-all carries its count in the accessible name — "Clear all 3 filters".
- Focus after removal moves to the next tag, or to Add filter when the last one goes. It never falls to the document body.
- Tags are never colour-coded by dimension alone; the dimension is in the text.
- Never brand coral, never an alarm hue on a filter tag. Filtering is not a clinical state — see Colour.
- Targets ≥ 24 px including the remove control inside the tag, with ≥ 24 px between adjacent remove controls (WCAG 2.2 SC 2.5.8).
- Tags wrap, never scroll horizontally. A filter you have to scroll to see is a filter you do not know about (SC 1.4.10).
Outcomes of use
What this contributes to, in the terms of Usability & context of use. These are attributes believed to contribute to an outcome; the outcome itself is settled by observing real use in a specified context, not by this page.
- Effectiveness — the clinician's belief about what they are looking at matches what they are looking at. This is the whole purpose of the component.
- Efficiency — a large set narrowed in one action, and restored in one action. Neither direction should cost a mental model.
- Satisfaction — no residual doubt about whether something is hidden, which is the doubt that sends clinicians to a second system to check.
Clinical safety notes
Trace these in your risk file (ISO 14971) and usability engineering file (IEC 62366-1).
- Active filters always rendered as readable tags. Mitigates: a filtered view mistaken for the complete record.
- Hidden quantity stated numerically at all times. Mitigates: the size of what is missing being unknowable.
- Alarms at advisory priority and above are unfilterable. Mitigates: a deteriorating patient hidden by a scoping choice made minutes earlier.
- Filters cleared on patient change. Mitigates: one patient's view constraints applied to another patient's record.
- Filters never restored silently at sign-in. Mitigates: a shift beginning inside a narrowed view nobody set today.
- Previous set retained while applying. Mitigates: a blank region read as an absence of patients.
- Zero-result state names the responsible filters. Mitigates: an over-narrow filter read as "nothing to do".
- Criteria written in clinical language. Mitigates: a filter misread under time pressure and left on.
Implementation
$ npx shadcn@latest add https://md.notjustany.tech/r/filter.json
// The escape rule is enforced in the component, not in each product's
// query layer, because it is the control that must not be forgotten.
<FilterBar
label="Worklist filters"
filters={active}
total={19}
shown={4}
onRemove={remove}
onClearAll={clearAll}
escaped={escapedAlarms} // items the filter would have hidden
/>
// Anything unacknowledged at advisory or above escapes the filter and is
// reported above the set. There is no flag to switch this off.
const escapes = (item) =>
item.alarm && !item.acknowledged &&
PRIORITY_ORDER[item.alarm] >= PRIORITY_ORDER.advisory
if (shown < total && escaped === undefined) {
throw new Error("[FilterBar] a narrowed set must compute its escaped items.")
}
| Prop | Type | Notes |
|---|---|---|
filters | ActiveFilter[] |
Required. Each carries dimension,
displayValue and id. |
total · shown | number |
Required. Both, always — the count line renders even at
19 of 19. |
escaped | EscapedItem[] |
Required when narrowed. Items above advisory priority the filter excluded. |
subjectId | string |
Filters clear automatically when this changes. |
savedViewName | string |
Renders a leading tag identifying the saved view as the source. |
Related
- Data grid — the primary consumer, and its column filters.
- List & tree — the same denominator rule at a smaller scale.
- Search — narrowing by query rather than by criterion.
- Empty state — what a filtered-to-zero set must say.
- Triage worklist — why handover is never filtered.
- Tokens & governance — why the escape rule has no override.