---
title: "Proposing a component"
description: "The intake questions a proposal must answer before an API is worth discussing. The four reasons a proposal is normally rejected."
url: "https://opsinjs.pensievelabs.org/handbook/contributing/proposing-a-component"
source: "https://opsinjs.pensievelabs.org/handbook/contributing/proposing-a-component.md"
section: "Handbook"
kind: "handbook"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["component proposal", "intake criteria", "new component request", "propose a new component", "propose a component", "request a component"]
---

> 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.
> Nothing is missing from this page. The data simply does not live in
> the prose.

<PageTemplate kind="handbook" />

## The short version [#the-short-version]

A proposal is a document, not a pull request, and it answers eight questions.
Answer them in order and the API falls out of the answers; start with the API
and the review will send you back to question one.

1. **What does it show, in one sentence a patient would understand?**
2. **What does it assert about somebody's health?** If the answer is "nothing",
   say so. That is a legitimate answer for a `Surface` and a wrong one for a
   `ScoreDial`.
3. **What must it never be read as?** The prohibition, stated as plainly as the
   intent.
4. **When should somebody *not* use it, and what should they use instead?** The
   alternative must be named. An entry with no alternative is not accepted.
5. **Which colour axis may it use, and who owns the thresholds?** See
   [The two colour axes](../../health/two-colour-axes.mdx).
6. **What is the accessibility bar?** Keyboard contract, announcement contract,
   what survives grayscale, what happens at 200% text.
7. **What is the failure mode when the data is empty, stale, partial or
   wrong?** Every one of the five [data
   states](../../foundations/data-states.mdx), with an answer each.
8. **What already exists that this is not?** Name the two closest components and
   the distinguishing line.

## How it works [#how-it-works]

Triage comes before the eight questions are worth reviewing, and it asks three
of its own.

**Is it distinct?** Name the closest existing component and say what it cannot
do. "Like `Callout` but for warnings" is not distinct, because the status axis
already carries urgency.

**Is it health-shaped?** opsinjs is not a general-purpose component library.
There are excellent ones already, and the reason to add something here is that
it carries a clinical meaning, a safety obligation or a plain-language contract
that a general component would get wrong.

**Would it be needed twice?** One screen in one product is a good local
component. Two unrelated products needing the same thing is a system component.

A proposal that clears those becomes a specification page at `status: planned`:
the eight answers above, expanded into the component page's outline, with a
proposed API as a fenced TypeScript block explicitly marked as a specification.
At that point it has a URL and a badge, and it can be argued with in public.
That is the expensive rung and the one that decides most proposals.

Moving from `planned` to `shipped` means code, and the bar for that is
[the component checklist](./component-checklist.mdx).

### The four common rejections [#the-four-common-rejections]

* **It is a variant of something that exists.** A "warning card" is
  `AlertBanner` with a status. Adding it as a component gives the same idea two
  names and two behaviours.
* **It carries clinical meaning we cannot support.** A component that returns an
  assessment, a triage disposition or a likely cause is outside the scope of
  this system. See
  [Is opsinjs right for this project?](../../start/is-opsinjs-right.mdx).
* **It mixes the axes by construction.** A component whose single `variant` prop
  selects both a metric family and a severity cannot be fixed by renaming the
  prop.
* **Nobody owns the thresholds.** A component that shows a value against a
  boundary needs a named owner for that boundary. "The design system decides" is
  not an answer.

## Do this [#do-this]

* **Bring a real screen.** A screenshot or a sketch of the product surface that
  needs this, with real content in it, answers half the questions before they
  are asked.
* **Write the "when not to use" list first.** It is the fastest way to find out
  whether the component is a real distinction or a restatement.
* **Propose the smallest thing.** A component that does one job composes; a
  component with a mode switch usually should have been two.
* **Accept "no" as a good result.** The commonest outcome is being told which
  existing component or pattern already does the job, which takes ten minutes
  rather than three months.
* **Say what it costs.** Whether it forces a client boundary, what it depends
  on, and roughly how much it weighs. See [Performance and bundle
  size](../performance-and-bundle-size.mdx).

## Not this [#not-this]

* **Do not open with a props table.** The API is the last question, and it is
  the easiest one once the first seven are answered.
* **Do not propose a component to hold a design opinion.** If the point is that
  a certain layout is better, that is a pattern or a foundation page.
* **Do not name it after its appearance.** `RoundedGaugeThing` will be wrong
  within a release. See [Naming
  conventions](../naming-conventions.mdx).
* **Do not skip question 7.** The empty, stale and partial states are where
  health components fail, and a proposal that has not thought about them is a
  proposal that has thought about the happy path.
* **Do not invent evidence for question 2.** If the reason is a design opinion,
  mark it as one.
* **Do not propose a general-purpose primitive** that a dozen mature libraries
  already ship better, and do not propose a rename of something that exists.
  Confusion about which component to use is a documentation defect, and the fix
  is the when-not-to-use list.
* **Do not justify a component with a screenshot of another product.** Say what
  the person reading the screen has to decide, and the proposal usually
  improves.

## Gotchas [#gotchas]

* **A component id is a URL, a catalogue row, a registry name and a set of
  frontmatter references.** Choosing it is a commitment; `assert-ia` will make
  the rename survivable but it is still a rename.
* **`implements` and `governedBy` must agree in both directions.** A doctrine
  page listing your component and a component page not listing the doctrine page
  fails the IA check.
* **Aliases live in `registry/catalogue.ts`, not in your page.** Global
  uniqueness is only maintainable with one owner.
* **A specification is reviewed by whoever the [freshness
  rules](../../project/docs-freshness.mdx) name for its kind**: design and
  engineering always, clinical for anything in a health category.

## Related [#related]

* [Component checklist](./component-checklist.mdx) says what `planned` has to
  clear to become code.
* [Components](../../components/index.mdx) is the roster as it stands.
* [Anatomy of a component page](../../components/anatomy-of-a-component-page.mdx)
  has the outline your specification will fill.
* [Roadmap](../../project/roadmap.mdx) is what is already agreed and ordered.
