opsinjs
ComponentsHealth communication

Term

A clinical word shown with its everyday meaning attached, so you can read a sentence without having to look anything up.

Preview

Theme
Density
Text
Status
term · base base · style base-lyraOpen under the product theme

Installation

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 callout instead.
  • The point is a standing statement about what this product is and is not.Use disclaimer-note instead.
  • You need general help text for a non-clinical interface control.Use tooltip instead.
  • The reader needs to browse or search every definition rather than read one in place.Use table instead.
  • What needs explaining is a number, a unit or a precision rather than a word.Use value instead.

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

  1. TermThe root. Resolves id against the provider's glossary and picks one of two presentations.Controlled by id
  2. Term.TriggerThe word as it reads in the sentence. A dotted-underlined button, or an unmarked span inline.Controlled by children
  3. Term.DefinitionThe plain-English sentence. In the accessibility tree whether open or not.Controlled by present
  4. Term.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.

Theme
Density
Text
Status
term-in-a-sentence · base base · style base-lyraOpen under the product theme

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

Theme
Density
Text
Status
term-an-abbreviation · base base · style base-lyraOpen under the product theme

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

KeyActionNotes
TabMove focus to the disclosure controlOne tab stop per term. The inline presentation has none.
Shift+TabMove focus backwards to itReverse order matches where the word sits in the sentence.
EnterShow or hide the definitionNative activation. The definition appears beside the word, not over it.
SpaceShow or hide the definitionNative activation, which is what stops the page scrolling.
EscapeHide the definitionConsumed only while open, so the sheet around it survives.
PairThemeAPCA LcWCAG 2.2Floor
body text on the pagelight101.617.18:1Pass
body text on the pagedark-100.518.00:1Pass
body text on a cardlight104.717.96:1Pass
body text on a carddark-99.616.32:1Pass
body text on the muted groundlight98.116.32:1Pass
body text on the muted grounddark-97.513.77:1Pass
secondary text on the pagelight83.97.42:1Pass
secondary text on the pagedark-80.713.44:1Pass
secondary text on a cardlight87.07.76:1Pass
secondary text on a carddark-79.812.19:1Pass
secondary text on the muted groundlight80.47.05:1Pass
secondary text on the muted grounddark-77.810.28:1Pass
a hairline boundary on the pagelight19.31.41:1Below floor
a hairline boundary on the pagedark-8.21.90:1Below floor
a hairline boundary on a cardlight22.41.47:1Below floor
a hairline boundary on a carddark-7.31.72:1Below floor
a hairline boundary on the muted groundlight15.81.34:1Below floor
a hairline boundary on the muted grounddark0.01.45:1Below floor
a placeholder boundary on the pagelight69.84.52:1Pass
a placeholder boundary on the pagedark-51.07.62:1Pass
a placeholder boundary on a cardlight72.84.72:1Pass
a placeholder boundary on a carddark-50.16.91:1Pass
a placeholder boundary on the muted groundlight66.24.29:1Pass
a placeholder boundary on the muted grounddark-48.15.83:1Pass
a placeholder fill on the pagelight8.31.18:1Below floor
a placeholder fill on the pagedark-8.21.90:1Below floor
a placeholder fill on a cardlight11.41.23:1Below floor
a placeholder fill on a carddark-7.31.72:1Below floor
a placeholder fill on the muted groundlight0.01.12:1Below floor
a placeholder fill on the muted grounddark0.01.45:1Below floor
a placeholder boundary on its own filllight58.93.84:1Pass
a placeholder boundary on its own filldark-41.54.01:1Below floor
the card hairline on the pagelight69.84.52:1Pass
the card hairline on the pagedark-28.74.19:1Below floor
the card hairline on a cardlight72.84.72:1Pass
the card hairline on a carddark-27.93.80:1Below 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.

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

On this page