Worklist
The scrolling patient list from qTrack — a column of rows with one of them current.
Rohan Sharma
Anika Desai
Vikram Iyer
Meera Krishnan
Installation
npx shadcn@latest add https://qure-ui.qure.ai/r/worklist.jsonUsage
import {
Worklist, WorklistItem, WorklistItemAvatar, WorklistItemContent,
WorklistItemMeta, WorklistItemName, WorklistItemSeparator,
} from '@/components/ui/worklist'<Worklist aria-label="Patients" value={selected} onValueChange={setSelected}>
<WorklistItem value={patient.uid} unread={patient.unreadCount}>
<WorklistItemAvatar>{initials}</WorklistItemAvatar>
<WorklistItemContent>
<WorklistItemName>{patient.name}</WorklistItemName>
<WorklistItemMeta>
<span className="shrink-0">M</span>
<WorklistItemSeparator />
<span className="truncate">{patient.accession}</span>
</WorklistItemMeta>
</WorklistItemContent>
</WorklistItem>
</Worklist>This one comes from the app rather than from Figma. It is
src/components/patient-worklist/ in qtrack-lc with the data layer left behind — the store, the
debounce and the analytics are the app's; the row is the design system's.
It is a listbox, not a nav
Choosing a row swaps what the panel beside it shows, which is a selection, so this is a listbox
and each row an option. If yours changes the URL instead, you want
NavList — role="option" on a router link tells a screen reader
there is a list to arrow through and then navigates out from under them.
role="option" makes a promise the app does not currently keep: its rows are click handlers
with no tabIndex, so nothing in the list is reachable from the keyboard at all. The role says
"arrow through this"; there is nothing to arrow.
Fixed here. The selected row holds the only tab stop, the arrow keys move focus, Home and End jump to the ends, Enter and Space select — the roving-tabindex pattern the APG asks for. Worth carrying back into qTrack.
Keyboard
| Key | Does |
|---|---|
Tab | Enters at the selected row, or the first one. |
↑ ↓ | Move focus. Selection follows on Enter, not on focus. |
Home End | First and last row. |
Enter Space | Select the focused row. |
Focus and selection are deliberately separate. In a follow-focus listbox, arrowing through 40 patients loads 40 patient records; here you can look before you commit.
Loading
loading puts aria-busy on the list, and the skeletons are aria-hidden. Both matter: a
listbox may only contain options and groups, so eight placeholder divs inside one are invalid
markup that reads aloud as nothing at all — an empty list, which is the wrong answer. aria-busy
is what says something is on its way.
The app's skeleton draws a 36px disc where its own row draws 28px, so the placeholder is wider than the thing it stands in for and the list nudges sideways when the data arrives. This one is 28px.
In a panel
Rohan Sharma
Anika Desai
Vikram Iyer
The whole shape, roughly as qTrack assembles it: a search field, the list, and Empty when nothing matches.
The search field is a plain Input — there is no WorklistSearch here,
because the app's version is a 500ms debounce around a store write, and none of that is a
component. Debounce in your own code and pass the result down.
The meta line
Separate the parts with WorklistItemSeparator rather than typing a |. A literal pipe between
two spans is a character in the accessible name, and "M vertical line ACC 482913" is how some
screen readers will say it.
Unread
unread takes a count or true. Pass the count where you have one — a dot on its own says
something happened, a number says how much, and the difference decides whether someone opens the
row now or later.
API Reference
Worklist
Renders the role="listbox" container.
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | The selected row. Pair with onValueChange. |
defaultValue | string | — | Selected on first render, when uncontrolled. |
onValueChange | (value: string) => void | — | Fires on click, Enter and Space. |
loading | boolean | false | Marks the list busy while placeholders are up. |
Give it an aria-label. An unlabelled listbox is announced as "listbox" and nothing else.
WorklistItem
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Required. Identifies the row. |
unread | boolean | number | — | Shows the dot. A number is announced. |
disabled | boolean | false | Sets aria-disabled and drops it out of the arrow order. |
A selected row scrolls itself into view with block: "nearest", which matters when the selection
arrived from a deep link. Not center — centring scrolls the list even when the row is already
visible, and that reads as the page moving for no reason.
The rest
WorklistItemAvatar is the initials disc, WorklistItemContent stacks the two lines,
WorklistItemName and WorklistItemMeta are the lines themselves, WorklistItemSeparator is the
divider, and WorklistItemSkeleton is a placeholder row at the same height.