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
Remove the criteria one at a time. The count never stops naming the whole set, and the escaped alarm stays until nothing is hidden — because it was never hidden by the filter in the first place.
| 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. |
How the criteria combine
Three tags reading Ward: Emergency, Awaiting review and Arrived after
12:00 describe a set. They do not say which set. Is it patients matching all three, or
patients matching any of them? The two answers differ by a factor of several, and the count line
cannot tell them apart — 4 of 19 is equally consistent with both.
The combining rule is written beside the tags, in words. It is never left to be inferred from the tags themselves, because the inference is a coin toss and the reader has no way to check which way it landed.
The convention this system uses, and states rather than assumes:
- Several values within one dimension are any. Ward: Emergency or Cardiology. Choosing a second ward widens the set, which is what a clinician selecting two wards expects.
- Different dimensions are all. Ward and status and arrival time each narrow further. Adding a dimension always shrinks the set.
- Both are stated on screen — "Emergency or Cardiology · awaiting review · arrived after 12:00" reads as a sentence and disambiguates itself. Tags in a row do not.
The failure this prevents is quiet and one-directional. A clinician who believes the criteria are any when they are all thinks they are looking at a wider set than they are — which is precisely the belief this component exists to stop them holding.
A filter someone else chose
Filter state belongs in the URL, for the same reasons as Pagination: back and refresh behave, and a view can be sent to a colleague. That second reason introduces the one case this component is not otherwise designed for.
Every rule on this page assumes the reader set the filter and may have forgotten. A shared link inverts that: they arrive at a narrowed set they never chose and have no memory of choosing, and the tags that would normally jog a memory are jogging nothing.
So a view arriving with filters already applied says so on arrival, in a statement rather than a tag row — "You have opened a filtered view: 4 of 19 patients. Show all." — and it says it once, prominently, rather than relying on chrome the reader has not yet learned to read.
- The same applies to a saved or default view. A deployment that ships a pre-filtered worklist has made the same choice on the clinician's behalf, and owes the same statement.
- Filters never survive a change of patient or a change of shift, per Persistence. A URL is a way of sharing a view, not a way of smuggling one past that rule.
- A link that no longer resolves to the same set says so. Criteria referring to something that has since changed — a ward that closed, a clinician who left — are reported rather than dropped silently, because a filter that quietly stopped applying is a filter the reader still believes is on.
Filter or search
They look similar and answer different questions, and the difference matters most when neither returns anything.
| Filter | Search | |
|---|---|---|
| Question | Which of these match my criteria? | Where is the thing I am looking for? |
| Operates on | A known set, by attribute | A body of content, by what it says |
| Empty result means | Nothing here matches these criteria — the set is intact and the criteria are wrong | It is not here, or it is called something else — which is a different thing to do next |
| Reversal | Remove a criterion; the set widens predictably | Change the terms; what comes back is not predictable |
Because they differ, so do their empty states. A filter with no matches says which criteria excluded everything and offers to remove them. A search with no results says what was searched and over what, per Empty state. Neither is an error, and neither is ever rendered as one — a red field on a filter that simply matched nothing tells the clinician they did something wrong when they did not.
The control and its tags
- Never an icon-only filter control. Offered widely for constrained space, and it fails here twice — this system has no icon set, and a filter is the one control whose purpose a reader must be able to name before pressing it. If the space is that tight, the screen has a layout problem rather than a labelling opportunity.
- Tags never truncate. The usual advice is to clip a long tag and put the full text in a tooltip; a tag reading Ward: Emerg… is a criterion the reader cannot verify, on a touch surface where the tooltip does not exist. Tags wrap onto further rows, which is what the rows are for.
- Tags are not reorderable. Their order carries no meaning, so dragging them achieves nothing and costs a single-pointer alternative under SC 2.5.7.
- Options are ordered by likelihood, then alphabetically past a dozen entries. A list a clinician has to read end-to-end every time is a list that gets guessed at.
- The count is announced when it changes, politely, as one summary — the number is the whole point of the component and a sighted reader gets it for free.
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. |
What the component enforces, and what it only asks for
A filter bar reports what is being hidden. It does not do the hiding, and the rules about what a filter may not conceal are enforced upstream of it.
| Rule | Held by | How |
|---|---|---|
| A filtered view states how much it is hiding | The component | hiddenCount is required. A filtered list cannot be rendered without saying the size of what it is not showing.Asserted: FilterBar: what is hidden is counted on screen |
| Alarms held visible despite the filter are named | The component | alarmsHeldVisible renders the count and the reason beside the tags.Asserted: FilterBar: alarms are never filtered away |
| A filter inherited from a link is marked as inherited | The component | inherited renders the notice. What it cannot know is whether the filter really came from a link rather than from this reader.Asserted: FilterBar: an inherited view says the reader did not choose it |
| Removing a tag removes that tag | The component | Each tag’s control calls onRemove with its own id and nothing else’s; a reader who removes “Ward 4” is still looking at the site they chose. Driven in a browser.Asserted: FilterBar: removing a tag removes that tag and no other |
| Removing a tag does not throw focus away | The component | Focus moves to the tag now in that position, or the one before it, or to the bar’s label once none are left. The removed tag took focus with it to <body>. Driven in a browser.Asserted: FilterBar: removing a tag by keyboard leaves focus in the bar |
| An alarm is never hidden by a filter | You | The component is told a number. Deciding that an unacknowledged alarm bypasses the predicate happens in your query, and nothing here can check that it did. |
| Removing a tag re-runs the query | You | onRemove is a callback. What happens next is yours. |
| Filter state does not survive a user switch | You | There is no storage here. A filter that persists across a sign-out is a decision made somewhere else. |
A rule stated on a component page reads as a rule the component keeps. 3 of these 6 are; 3 are obligations this page was quietly handing to the reader. The safety rule on this page — that a filter may not conceal an alarm — is entirely yours. The component can only report a number you calculated correctly.
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.
Emergency or Cardiology, and awaiting review · showing 4 of 19
The sentence disambiguates what the tags cannot. A reader can tell at a glance whether adding a ward widened or narrowed the set.
Showing 4 of 19
Three tags and a count, consistent with two very different sets. The reader who assumes "any" believes they are seeing more than they are.
Arriving from a colleague's link, the reader never chose this filter and has no memory to jog. It says so once, plainly.
The tag row alone. Every rule on this page assumes the reader set the filter and might have forgotten — here they never set it at all.
Wraps to a second row. The criterion is readable and therefore checkable, on any device.
Clipped, with the rest in a tooltip that does not exist on a cart. The clinician cannot verify which ward they filtered to.
No patients match these criteria
Ward: Emergency and arrived after 18:00 exclude all 19. Remove the time criterion
Names which criteria emptied the set and offers the way back. The set is intact; the criteria were wrong.
Rendered as a validation error. The clinician did nothing wrong, and now believes they did.
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). Which of these the component keeps and which are yours is stated in the guarantees table; a control the component does not keep is one your risk file must show you keep.
- Active filters always rendered as readable tags. (the component) Mitigates: a filtered view mistaken for the complete record.
- Hidden quantity stated numerically at all times. (the component) Mitigates: the size of what is missing being unknowable.
- Alarms at advisory priority and above are unfilterable. (you) Mitigates: a deteriorating patient hidden by a scoping choice made minutes earlier.
- Filters cleared on patient change. (you) Mitigates: one patient's view constraints applied to another patient's record.
- Filters never restored silently at sign-in. (you; the component marks an inherited set as inherited) Mitigates: a shift beginning inside a narrowed view nobody set today.
- Previous set retained while applying. (you) Mitigates: a blank region read as an absence of patients.
- Zero-result state names the responsible filters. (you) Mitigates: an over-narrow filter read as "nothing to do".
- Criteria written in clinical language. (you) Mitigates: a filter misread under time pressure and left on.
Implementation
$ npx shadcn@latest add https://md.notjustany.tech/r/filter.json
<FilterBar
active={filters} // each tag carries its own id and label
onRemove={remove}
onClearAll={clearAll}
hiddenCount={15} // required: how many rows the filter is hiding
alarmsHeldVisible={2} // shown despite the filter, and said so
inherited // this filter came from a link, not from you
/>
// 'hiddenCount' is required, so a filtered view cannot be rendered without
// stating the size of what it is not showing.
What the component takes. This table used to list filters, total, escaped, subjectId, savedViewName — props the component does not have, and never had. Found by the registry gate’s props-table rule.
| Prop | Type | Default | Notes |
|---|---|---|---|
active | FilterTag[] | — | Required. The filters in force, each a tag the reader can remove. |
onRemove | (id: string) => void | — | Required. Called with the removed tag’s id. Re-running the query is yours. |
onClearAll | () => void | — | Required. |
hiddenCount | number | — | Required. How many the filters are hiding, stated beside them. |
alarmsHeldVisible | number | 0 |
Alarms shown despite the filters, named as such. Keeping them visible is yours. |
combining | string | — | How the criteria combine, in words — “Emergency or Cardiology, and awaiting review”. |
inherited | boolean | false |
The reader arrived at an already-filtered view and did not set these criteria. |
Related
- Affordance & perceivability — the general rule this page applies: a control exists only if every reader who can operate it can perceive it.
- Data grid — the primary consumer, and its column filters.
- List — 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.