Badge
A count, and nothing else. The rules that matter are not about rendering the number — they are about the number being exact, and about the set it is exact about, which is where badges actually mislead.
Overview
A badge is a count and nothing else. It never carries clinical state, never replaces a Status chip, and never becomes an alarm by turning red. A count of three tells you there are three things; it does not tell you any of them is critical, and it must not pretend to.
Every rule below is about the number being exact, or about the set it is exact about. The second is the harder one and the one this component gets wrong most often — see What the count counts. Where a set is larger than the screen, Pagination says which part of it you are looking at.
Anatomy, and the rule that carries the risk
-
Laboratory 6 1 criticalCount and priority as two separate statements
-
Prior recordings 3A plain count, carrying no clinical claim
-
Imaging 0Zero is rendered, not hidden — "none ordered" is information
A badge never uses an --alarm-* colour to mean "some of these
are serious". A red badge is an alarm signal borrowed for a quantity, and it does two kinds of
damage: it dilutes the IEC 60601-1-8 palette everywhere else in the product, and it lets a
critical finding be represented by a number nobody has to open. Where priority exists, say it
in words with a status chip beside the count.
The single exception is documented on Colour and applies only to the alarm banner's own occurrence counter, where the surrounding component already carries the priority.
- Exact, never capped. "99+" hides an order of magnitude. If the number is too wide for the space, the space is wrong.
- Zero renders. A hidden zero is indistinguishable from an unloaded count, which is the ambiguity Key–value pair exists to prevent.
- Unknown is not zero. A count that failed to load renders as
—with a reason, never as0. - Never the only carrier of meaning. A badge always sits beside a label naming what is being counted.
- Never a dot alone for anything clinical. A dot says "something changed" and nothing else; it is permitted only for non-clinical notifications.
- Never on a disabled or unavailable control — a count on something you cannot open is a dead end.
What the count counts
Every rule above is about the number being exact. None of them is about the number being exact about what, which is where badges actually mislead. A badge is a claim with a hidden subject, and three things quietly change that subject.
A badge counts what its label names, over the whole set. Where a filter, a permission or a time window narrows what the reader can actually reach, the badge either counts the whole set anyway or states the narrowing. It never silently counts the visible subset.
A filter is active
STATES THE NARROWING
SILENTLY COUNTS THE SUBSET
The second badge is not wrong about anything it says. It is wrong about what it is about, and a clinician who set that filter twenty minutes ago has no way to tell. This is Filter's hidden-count rule arriving at a component that was designed without it in mind.
The reader cannot reach all of them
A count of forty results where the reader's role permits opening thirty-one is a count of things some of which do not exist for them. It reads as work they have not done. Either the badge counts what they can reach and says so, or it counts everything and states the difference — but the difference is stated, because Acting for someone else turns on a reader knowing what is being withheld and why.
A time window is implied
"6" beside Laboratory means six today, or six this admission, or six ever. The label carries the window or the badge does. Unscoped counts are read against whatever window the reader already had in mind, which is rarely the one the query used.
A count is a snapshot
A badge is only as current as its last fetch, and unlike a value in a Key–value pair it carries no timestamp of its own. On a worklist that polls every few minutes, a badge is a claim about the past presented in the present tense.
- Where the count drives a decision, its age is available. Not necessarily rendered beside every badge — that would be unreadable — but reachable, and rendered once for the surface as a whole: "counts as of 14:26:38".
- A failed refresh does not freeze a confident number. The count keeps its last value and the surface states that refreshing failed, per Pending value. A number that stopped updating and does not say so is worse than one that admits it.
- A count that has never loaded is not zero. Already the rule above; it is restated here because a polling failure at startup is the case that produces it.
- Do not animate the change. A count that ticks or flashes when it increments spends the movement budget that alarms depend on — see Motion. It updates, politely announced, and that is all.
Rendering the number
- Tabular figures, so a column of counts can be scanned for an outlier rather than read one at a time — see Typography.
- Group digits by locale, and never by hand. A thousands separator is a decimal point in several of the places this system claims to support, and a count is one of the few numbers where that ambiguity is survivable — which is exactly why it gets fixed here rather than argued about later.
- The accessible name is a sentence, and it agrees. "1 result", not "1 results"; the plural rules come from the platform's formatting, not from appending an s.
- The badge is never the target. It is content inside a row or a label, and the row is what is pressed. A 24 px badge made clickable is a control below the floor, sitting next to the larger control it will be mistaken for.
States
| State | Rendering |
|---|---|
| Counted | The exact number, grouped by locale. |
| Zero | Rendered as 0, never hidden. "None ordered" is information. |
| Unloaded or failed | — with an accessible reason, never 0. |
| Narrowed by a filter | The whole-set total, with what is hidden stated beside it — see What the count counts. |
| Stale | The last known good value, with the surface stating that refreshing failed. |
| Updating | Updates on data arrival without animation, and never reorders what it sits on. |
What the component enforces, and what it only asks for
A badge is a number and a noun, and the type holds both. What it cannot hold is whether the number is right, which is the whole of what a badge is for.
| Rule | Held by | How |
|---|---|---|
| A count always names what it counts | The component | label is required, in singular and plural forms, selected by Intl.PluralRules rather than by appending an s.Asserted: Badge: the announced name agrees in number |
| A count is never capped | The component | There is no max prop, so “99+” cannot be produced. The difference between 99 and 340 unreviewed studies is a busy afternoon and a service failure.Asserted: Badge: an exact count, never capped |
| It never becomes an alarm | The component | tone offers neutral and attention only; no alarm hue is reachable.Asserted: Badge: no alarm tone is reachable |
| It is not a control | The component | BadgeProps does not extend button or anchor props, so a 24 px badge cannot be made clickable beside the larger control it would be mistaken for.Asserted: Badge: is not a control — no click or href surface to pass |
| A stale count shows its age on screen | The component | asOf is visible beside the count as well as in its name. It was in the screen-reader text only, so a stale number was disclosed to one kind of reader and shown as current to everyone else.Asserted: Badge: the age of the count reaches the accessible name |
| The noun agrees in the language it is written in | The component | label takes every plural form a language has, and locale names the language, English by default. The rule was the browser’s, so English words were declined by French rules on a French workstation — “0 result” — and a language with more than two forms could not be written.Asserted: Badge: plural forms follow the language of the words, not the browser |
| The number is correct | You | It renders what you pass. A stale count is indistinguishable from a fresh one. |
| The set the number is about is the set the reader assumes | You | hiddenByFilter exists so you can say a filter is deciding what the number means — but nothing makes you pass it. |
| It is current | You | asOf is optional and is a string you supply. A badge is only as current as your last fetch and carries no clock of its own. |
A rule stated on a component page reads as a rule the component keeps. 4 of these 7 are; 3 are obligations this page was quietly handing to the reader. Every mechanical rule is held and every rule about truth is yours, which is roughly the shape of this whole system.
Do's and don'ts
Quantity and priority stated separately. Six results; one of them is critical; both facts survive greyscale.
A red count. It borrows the IEC critical hue to mean "some of these matter", and dilutes it everywhere the product uses it properly.
Measured zero and unknown are different renderings. One is a fact; the other is a gap.
A hidden zero and a failed count rendered as zero. The clinician reads "nothing to see" twice, and is wrong once.
Counts the whole set and states what is narrowing the view. The clinician can tell that the six in front of them are not all of it.
Exact, and about the wrong set. A filter set twenty minutes ago is silently deciding what this number means, and nothing on screen says so.
Worklist 12
Counts as of 14:26:38 · refresh failed at 14:31, showing the last successful count
The number kept its last real value and the surface admits it stopped updating. Both facts are needed to know what to do with it.
Worklist 12
Frozen at 14:26 and presented in the present tense. It has been wrong for five minutes and looks exactly as confident as it did when it was right.
The whole row is the control, comfortably past the floor. The badge is content inside it.
Prior recordings
A 24 px control sitting beside the larger one it will be mistaken for, and below the target floor in gloves.
Accessibility
- A badge is never a bare number. Its accessible name includes what is counted — "Laboratory, 6 results" — because "6" announced alone is meaningless.
- Badges that change are announced politely (
aria-live="polite") or not at all. Never assertive: a count is not an interruption. - Never colour alone. Badge styling carries no meaning, so there is nothing for colour to fail to convey — which is the point.
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 knows the true size of the set and is told about anything urgent they cannot currently see.
- Efficiency — a count answers "is it worth opening?" without opening anything, which is most of what counts are for.
- Satisfaction — exact numbers. A capped or approximate count is the kind of small dishonesty that makes people stop believing the rest of the screen.
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.
- Badges never take an alarm hue. (the component) Mitigates: dilution of the IEC 60601-1-8 palette, and priority conveyed by a number nobody opens.
- Counts are exact and uncapped. (the component) Mitigates: a set's true size being unknowable at a glance.
- Zero rendered; unknown rendered differently. (you) Mitigates: a failed count read as an absence of results.
- The count is over the whole set, or states what narrows it. (you; the component names what it counts) Mitigates: a number that is exact about a set the reader did not have in mind.
- A stale count says so. (you) Mitigates: a frozen number presented in the present tense after a failed refresh.
- The badge is never the target. (the component) Mitigates: a control below the size floor beside the larger one it will be mistaken for.
Implementation
$ npx shadcn@latest add https://md.notjustany.tech/r/badge.json
// A count and the thing being counted. No variant prop, because a badge
// has no clinical variants — and no max, so "99+" cannot be produced.
<Badge count={6} label="results" />
// A filter is narrowing the set: count stays the whole-set total.
<Badge count={40} label="results" hiddenByFilter={34} />
// Both plural forms, so the announced name agrees.
<Badge count={1} label={{ one: "result", other: "results" }} />
Related
- Pagination — which part of the counted set you are looking at.
- Status chip — where clinical priority belongs instead.
- Filter — the same "what am I not seeing?" rule, applied to criteria.
- List — counts on branches, and the denominator rule.
- Colour — why alarm hues are reserved.
- Typography — tabular numerals in counts.