Components · Data

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.

Stable · v1.0 shadcn/ui WCAG 2.2 AA IEC 62366-1

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

Remove a criterion and watch the denominator move

Showing 4 of 19 patients · 15 hidden by filters

1 critical alarm outside your current filters
Bed 7 · ST elevation V2–V4 · unacknowledged
Shown regardless of filters · detected 15:41:02

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.

PartRule
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

The rule that carries the risk

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.

Persistence

A filter that outlives the clinician's memory of setting it is the hazard. So persistence is deliberately weak, and always announced.

BoundaryBehaviour
Within a view, same sessionFilters persist.
Navigating away and back Filters persist and the tag row is re-announced on return.
Patient changeCleared. Without exception.
New session or new userCleared. Filters are never part of a saved user profile that applies silently at sign-in.
Shift handoverCleared, and the handover view is never filtered — see Triage worklist.
Saved viewsPermitted, 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 rule

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:

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.

The inherited filter

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.

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

States

StateRendering
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.

RuleHeld by How
A filtered view states how much it is hidingThe 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 namedThe 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 inheritedThe 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 tagThe 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 awayThe 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 filterYou 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 queryYou onRemove is a callback. What happens next is yours.
Filter state does not survive a user switchYou There is no storage here. A filter that persists across a sign-out is a decision made somewhere else.
Why this is written down

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

Do
Ward: Emergency Awaiting review

Each criterion legible and separately removable, sitting directly above the data it narrows.

Don't

Two criteria hidden behind a number. The clinician can see that something is filtered, which is exactly the state that gets misremembered as nothing.

Do

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.

Don't

4 patients

A count with no denominator. Indistinguishable from a ward with four patients in it.

Do
1 critical alarm outside your filters — Bed 7

The filter narrowed the view, not the duty of care. The alarm escapes and says that it did.

Don't

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.

Do
Arrived after 12:00

The criterion in the clinician's own words, complete enough to check against what they meant.

Don't
arrival_ts > 1200

A field name and a raw value. Unreadable under time pressure, ambiguous about time zone, and unlabelled for assistive technology.

Do
Ward: Emergency Ward: Cardiology Awaiting review

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.

Don't
Ward: Emergency Ward: Cardiology Awaiting review

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.

Do
You have opened a filtered view
4 of 19 patients · 15 hidden. Show all

Arriving from a colleague's link, the reader never chose this filter and has no memory to jog. It says so once, plainly.

Don't
Ward: Emergency

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.

Do
Ward: Emergency Assessment Unit

Wraps to a second row. The criterion is readable and therefore checkable, on any device.

Don't
Ward: Emergency Assessment Unit

Clipped, with the rest in a tooltip that does not exist on a cart. The clinician cannot verify which ward they filtered to.

Do

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.

Don't
No results found

Rendered as a validation error. The clinician did nothing wrong, and now believes they did.

Accessibility

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.

Clinical safety notes

Risk controls this page specifies

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.

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.

PropTypeDefaultNotes
activeFilterTag[]— 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.
hiddenCountnumber— Required. How many the filters are hiding, stated beside them.
alarmsHeldVisiblenumber0 Alarms shown despite the filters, named as such. Keeping them visible is yours.
combiningstring— How the criteria combine, in words — “Emergency or Cardiology, and awaiting review”.
inheritedbooleanfalse The reader arrived at an already-filtered view and did not set these criteria.
NotJustAnyMed.Tech Design System · Filter · v1.0 · draft for review
Reference applications named in this system are fictional; all patient data and reference ranges shown are fabricated and illustrative.