Term
A clinical word shown with its everyday meaning attached, so you can read a sentence without having to look anything up.
Preview
term · base base · style base-lyraOpen under the product themeInstallation
pnpm dlx shadcn@latest add @opsinjs/term
The @opsinjs namespace is declared in your components.json. Everything it installs is code you then own. There is no runtime package to keep in step.
Usage
import { Term, TermGlossaryProvider } from "@/components/ui/term"<TermGlossaryProvider glossary={GLOSSARY}>
<p>
An <Term id="acute" /> problem started suddenly.
</p>
</TermGlossaryProvider>When to use it
Use it when
- A clinical word the reader will meet on a letter, in a consultation, or on a test result, so a plainer word cannot replace it.
- An abbreviation whose expansion the reader needs at least once: HbA1c, eGFR, BP, SpO2.
- A word whose everyday meaning differs from its clinical one, such as 'chronic' or 'acute'.
Do not use it when
- The explanation runs past a sentence or two, or carries a caveat as well as a definition.Use
calloutinstead. - The point is a standing statement about what this product is and is not.Use
disclaimer-noteinstead. - You need general help text for a non-clinical interface control.Use
tooltipinstead. - The reader needs to browse or search every definition rather than read one in place.Use
tableinstead. - What needs explaining is a number, a unit or a precision rather than a word.Use
valueinstead.
Clinical meaning
Asserts. That this word has this everyday meaning in the product's glossary. That translation is the product's claim, not a clinician's, and opsinjs ships an empty glossary. See Who this is for.
Never read as. A statement about the reader's condition, or advice. Chronic means long-lasting is a translation; chronic means you will have this for life is a prognosis the glossary is not for.
Colour axis. Neither. The mark is a dotted underline that survives greyscale, because tinting jargon by category or status would invent a clinical level nobody assigned. See Clinical interaction guidelines.
Thresholds. None. Term puts no reading on the screen, so it holds no threshold and stamps no value.
Gap you must build around. A call site naming a plain-only entry gets the authored
sentence and a development warning, because a runtime cannot rewrite one. No lint rule
catches it.
Anatomy
TermThe root. Resolves id against the provider's glossary and picks one of two presentations.Controlled byidTerm.TriggerThe word as it reads in the sentence. A dotted-underlined button, or an unmarked span inline.Controlled bychildrenTerm.DefinitionThe plain-English sentence. In the accessibility tree whether open or not.Controlled bypresentTerm.ExpansionThe full form of an abbreviation, inside the definition and before it.
- Term 1data-slot="term"
- Term.Trigger 0-1A button with aria-expanded and aria-describedby, or a span. Absent on an unknown or plain-only entry.
- Term.Definition 0-1The trigger's next sibling. Absent on the same two paths.
- Term.Expansion 0-1Present only when the entry carries an expansion.
Examples
Reading a paragraph without leaving it
Two terms is near the ceiling for one paragraph. A third carries once, so the word stays
marked and reachable but its definition is not reprinted.
term-in-a-sentence · base base · style base-lyraOpen under the product themeAn abbreviation needs two things
The expansion says what the letters stand for, the definition what the thing is. Both
entries are marked showBoth: "always", so both arrive inline. SpO2 adds speech.
term-an-abbreviation · base base · style base-lyraOpen under the product themeContent guidelines
Definitions are one sentence under about twenty words: define the word, not the condition,
and never nest a second piece of jargon. Expansions are the full form
and nothing else. auto keeps a definition of about eighty characters or less beside the
word, so a tight sentence needs no press.
Do
"eGFR (estimated glomerular filtration rate, an estimate of how well your kidneys are filtering)." Expansion, then plain definition.
Don’t
"eGFR (estimated glomerular filtration rate)." Every word in the expansion needs its own definition.
Accessibility
Audited against WCAG 2.2 AA in a source pass and a rendered pass. The audit is author-run, not independent, and clinical review is still pending.
Fixed by the audit. The disclosure trigger's focus ring hardcoded a 2px width and
offset, ignoring the --opsin-border-focus and --opsin-border-focus-offset tokens. It
now honours them, so the indicator grows with a reader's focus width.
Verified in source. present has no hover value, so the definition never depends on
hover. Collapsed, it is display: none yet still reaches the reader as the trigger's
description. The disclosure is a real button named by its visible word, meeting SC 2.5.3
Label in Name. The mark changes form, not colour: dotted at rest, solid while open.
Residual, and stated plainly. The inline trigger falls under the 44 by 44 floor and
relies on the SC 2.5.8 inline exception, and adjacent wrapped triggers can overlap. aria-controls deliberately points at the closed display: none panel. No assistive technology has been tested against it, so the lived experience
is unverified.
| Key | Action | Notes |
|---|---|---|
| Tab | Move focus to the disclosure control | One tab stop per term. The inline presentation has none. |
| Shift+Tab | Move focus backwards to it | Reverse order matches where the word sits in the sentence. |
| Enter | Show or hide the definition | Native activation. The definition appears beside the word, not over it. |
| Space | Show or hide the definition | Native activation, which is what stops the page scrolling. |
| Escape | Hide the definition | Consumed only while open, so the sheet around it survives. |
| Pair | Theme | APCA Lc | WCAG 2.2 | Floor |
|---|---|---|---|---|
| body text on the page | light | 101.6 | 17.18:1 | Pass |
| body text on the page | dark | -100.5 | 18.00:1 | Pass |
| body text on a card | light | 104.7 | 17.96:1 | Pass |
| body text on a card | dark | -99.6 | 16.32:1 | Pass |
| body text on the muted ground | light | 98.1 | 16.32:1 | Pass |
| body text on the muted ground | dark | -97.5 | 13.77:1 | Pass |
| secondary text on the page | light | 83.9 | 7.42:1 | Pass |
| secondary text on the page | dark | -80.7 | 13.44:1 | Pass |
| secondary text on a card | light | 87.0 | 7.76:1 | Pass |
| secondary text on a card | dark | -79.8 | 12.19:1 | Pass |
| secondary text on the muted ground | light | 80.4 | 7.05:1 | Pass |
| secondary text on the muted ground | dark | -77.8 | 10.28:1 | Pass |
| a hairline boundary on the page | light | 19.3 | 1.41:1 | Below floor |
| a hairline boundary on the page | dark | -8.2 | 1.90:1 | Below floor |
| a hairline boundary on a card | light | 22.4 | 1.47:1 | Below floor |
| a hairline boundary on a card | dark | -7.3 | 1.72:1 | Below floor |
| a hairline boundary on the muted ground | light | 15.8 | 1.34:1 | Below floor |
| a hairline boundary on the muted ground | dark | 0.0 | 1.45:1 | Below floor |
| a placeholder boundary on the page | light | 69.8 | 4.52:1 | Pass |
| a placeholder boundary on the page | dark | -51.0 | 7.62:1 | Pass |
| a placeholder boundary on a card | light | 72.8 | 4.72:1 | Pass |
| a placeholder boundary on a card | dark | -50.1 | 6.91:1 | Pass |
| a placeholder boundary on the muted ground | light | 66.2 | 4.29:1 | Pass |
| a placeholder boundary on the muted ground | dark | -48.1 | 5.83:1 | Pass |
| a placeholder fill on the page | light | 8.3 | 1.18:1 | Below floor |
| a placeholder fill on the page | dark | -8.2 | 1.90:1 | Below floor |
| a placeholder fill on a card | light | 11.4 | 1.23:1 | Below floor |
| a placeholder fill on a card | dark | -7.3 | 1.72:1 | Below floor |
| a placeholder fill on the muted ground | light | 0.0 | 1.12:1 | Below floor |
| a placeholder fill on the muted ground | dark | 0.0 | 1.45:1 | Below floor |
| a placeholder boundary on its own fill | light | 58.9 | 3.84:1 | Pass |
| a placeholder boundary on its own fill | dark | -41.5 | 4.01:1 | Below floor |
| the card hairline on the page | light | 69.8 | 4.52:1 | Pass |
| the card hairline on the page | dark | -28.7 | 4.19:1 | Below floor |
| the card hairline on a card | light | 72.8 | 4.72:1 | Pass |
| the card hairline on a card | dark | -27.9 | 3.80:1 | Below floor |
These are the measured token pairs this component draws colour from, not a measurement of the component itself.
API reference
Prop
Type
Generated from TermProps in registry/bases/base/term.tsx.
Prop
Type
Generated from TermGlossaryProviderProps in registry/bases/base/term.tsx.
There is no way to supply a definition at a call site. Definitions live in the glossary
and arrive once through TermGlossaryProvider, so they stay consistent and reviewable. An id the glossary does not hold renders unmarked and warns in
development, printing children where a call site supplied any and otherwise the raw id.
Related
- Tooltip is a general hover or focus popup. Term avoids it, because a definition must not depend on hover.
- Callout is for an explanation longer than a sentence or two.
- Value formats numbers and units. Term explains words.
- DisclaimerNote is a standing statement about the product, not one word's meaning.
CareCard
A card that tells you what to do and when. It carries one instruction at one of three levels of urgency, with the urgency stated in the heading, not implied.
ConsentSheet
A sheet that asks permission for one specific thing at a time, explains what it is for, and makes saying no or changing your mind as easy as saying yes.