---
title: "Term"
description: "A clinical word shown with its everyday meaning attached, so you can read a sentence without having to look anything up."
url: "https://opsinjs.pensievelabs.org/components/term"
source: "https://opsinjs.pensievelabs.org/components/term.md"
section: "Components"
status: "shipped"
kind: "component"
category: "health-communication"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["glossary term", "jargon", "plain english", "definition", "tooltip term", "plain word"]
governedBy: ["who-this-is-for", "clinical-interaction-guidelines"]
usedIn: ["ask-users-for/ethnicity", "ask-users-for/medications", "ask-users-for/sex-and-gender", "ask-users-for/symptoms", "choose-a-component", "consent-and-permissions", "consent-flow", "diabetes-medicines-app", "onboarding-screen", "result-disclosure", "results-screen", "trends-screen", "value-against-a-range"]
implemented: true
---

> Elements written as `<PascalCase … />` below are opsinjs documentation
> components. Their attributes are the content: the values they render are
> generated from `tokens/*.json` and `registry/catalogue.ts` and are
> published separately at https://opsinjs.pensievelabs.org/r/index.json and under the Reference
> section.
> `<StubNotice>` IS THE EXCEPTION, AND IT IS THE ONE TO READ. It is a
> paired element rather than a self-closing one, and the text between
> its opening and closing tags is prose an author wrote, reproduced
> below word for word. That prose is where this page says whether the
> component has been reviewed. Read the children, not only the
> attributes.

<StubNotice
  name="term"
  status="shipped"
  questions="[
  &#x22;A collapsed definition reaches a reader only through the trigger's accessible description, and no real screen reader on a real phone has ever tested that claim.&#x22;,
  &#x22;The inline trigger never reaches a 44 by 44 hit area, and whether that exception is the right trade for a reader with a tremor is untested.&#x22;,
  &#x22;The glossary's policy rule says the plain wording should be the visible text and the clinical word the annotation, which is the opposite of this component's shape.&#x22;,
]"
>
  Audited against WCAG 2.2 AA. Clinical review pending.
</StubNotice>

## Preview [#preview]

<ComponentPreview name="term" />

## Installation [#installation]

<ComponentInstall name="term" unbuilt="false" importPath="@/components/ui/term" />

## Usage [#usage]

```tsx
import { Term, TermGlossaryProvider } from "@/components/ui/term"
```

```tsx
<TermGlossaryProvider glossary={GLOSSARY}>
  <p>
    An <Term id="acute" /> problem started suddenly.
  </p>
</TermGlossaryProvider>
```

## When to use it [#when-to-use-it]

<WhenToUse
  use="[
  &#x22;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.&#x22;,
  &#x22;An abbreviation whose expansion the reader needs at least once: HbA1c, eGFR, BP, SpO2.&#x22;,
  &#x22;A word whose everyday meaning differs from its clinical one, such as 'chronic' or 'acute'.&#x22;,
]"
  avoid="[
  { case: &#x22;The explanation runs past a sentence or two, or carries a caveat as well as a definition.&#x22;, instead: &#x22;callout&#x22; },
  { case: &#x22;The point is a standing statement about what this product is and is not.&#x22;, instead: &#x22;disclaimer-note&#x22; },
  { case: &#x22;You need general help text for a non-clinical interface control.&#x22;, instead: &#x22;tooltip&#x22; },
  { case: &#x22;The reader needs to browse or search every definition rather than read one in place.&#x22;, instead: &#x22;table&#x22; },
  { case: &#x22;What needs explaining is a number, a unit or a precision rather than a word.&#x22;, instead: &#x22;value&#x22; },
]"
/>

## Clinical meaning [#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](../health/who-this-is-for.mdx).

**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](../health/clinical-interaction-guidelines.mdx).

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

<SafetyCallout severity="attention" evidence="opinion">
  Review the glossary as clinical content, not as microcopy. A definition that slides into
  guidance has nobody behind it, and nothing catches the drift.
</SafetyCallout>

## Anatomy [#anatomy]

<Anatomy
  name="term"
  parts="[
  {
    name: &#x22;Term&#x22;,
    describes: &#x22;The root. Resolves id against the provider's glossary and picks one of two presentations.&#x22;,
    prop: &#x22;id&#x22;,
  },
  {
    name: &#x22;Term.Trigger&#x22;,
    describes: &#x22;The word as it reads in the sentence. A dotted-underlined button, or an unmarked span inline.&#x22;,
    prop: &#x22;children&#x22;,
  },
  {
    name: &#x22;Term.Definition&#x22;,
    describes: &#x22;The plain-English sentence. In the accessibility tree whether open or not.&#x22;,
    prop: &#x22;present&#x22;,
  },
  {
    name: &#x22;Term.Expansion&#x22;,
    describes: &#x22;The full form of an abbreviation, inside the definition and before it.&#x22;,
  },
]"
/>

<CompositionTree
  name="term"
  tree="[
  {
    part: &#x22;Term&#x22;,
    cardinality: &#x22;1&#x22;,
    note: &#x22;data-slot=\&#x22;term\&#x22;&#x22;,
    children: [
      {
        part: &#x22;Term.Trigger&#x22;,
        cardinality: &#x22;0-1&#x22;,
        note: &#x22;A button with aria-expanded and aria-describedby, or a span. Absent on an unknown or plain-only entry.&#x22;,
        children: [],
      },
      {
        part: &#x22;Term.Definition&#x22;,
        cardinality: &#x22;0-1&#x22;,
        note: &#x22;The trigger's next sibling. Absent on the same two paths.&#x22;,
        children: [
          {
            part: &#x22;Term.Expansion&#x22;,
            cardinality: &#x22;0-1&#x22;,
            note: &#x22;Present only when the entry carries an expansion.&#x22;,
          },
        ],
      },
    ],
  },
]"
/>

## Examples [#examples]

### Reading a paragraph without leaving it [#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.

<ComponentPreview name="term-in-a-sentence" kind="example" align="start" />

### An abbreviation needs two things [#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`.

<ComponentPreview name="term-an-abbreviation" kind="example" align="start" />

## Content guidelines [#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.

<DoDont>
  <DoDont.Do>
    **"eGFR (estimated glomerular filtration rate, an estimate of how well your kidneys
    are filtering)."** Expansion, then plain definition.
  </DoDont.Do>

  <DoDont.Dont>
    **"eGFR (estimated glomerular filtration rate)."** Every word in the expansion needs
    its own definition.
  </DoDont.Dont>
</DoDont>

## Accessibility [#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.

<KeyboardTable
  name="term"
  rows="[
  {
    keys: &#x22;Tab&#x22;,
    action: &#x22;Move focus to the disclosure control&#x22;,
    notes: &#x22;One tab stop per term. The inline presentation has none.&#x22;,
  },
  {
    keys: &#x22;Shift + Tab&#x22;,
    action: &#x22;Move focus backwards to it&#x22;,
    notes: &#x22;Reverse order matches where the word sits in the sentence.&#x22;,
  },
  {
    keys: &#x22;Enter&#x22;,
    action: &#x22;Show or hide the definition&#x22;,
    notes: &#x22;Native activation. The definition appears beside the word, not over it.&#x22;,
  },
  {
    keys: &#x22;Space&#x22;,
    action: &#x22;Show or hide the definition&#x22;,
    notes: &#x22;Native activation, which is what stops the page scrolling.&#x22;,
  },
  {
    keys: &#x22;Escape&#x22;,
    action: &#x22;Hide the definition&#x22;,
    notes: &#x22;Consumed only while open, so the sheet around it survives.&#x22;,
  },
]"
/>

<ContrastReport component="term" />

## API reference [#api-reference]

<PropsTable name="TermProps" />

<PropsTable name="TermGlossaryProviderProps" />

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 [#related]

* [Tooltip](./tooltip.mdx) is a general hover or focus popup. Term avoids it, because a
  definition must not depend on hover.
* [Callout](./callout.mdx) is for an explanation longer than a sentence or two.
* [Value](./value.mdx) formats numbers and units. Term explains words.
* [DisclaimerNote](./disclaimer-note.mdx) is a standing statement about the product, not
  one word's meaning.
