opsinjs

Versioning policy

What counts as a breaking change. It includes the rendered DOM, the data-* attributes and the CSS custom properties, not only the JavaScript API.

The short version

opsinjs follows semantic versioning, and the thing being versioned is larger than the JavaScript API. Because you own the source of every component you install, and because your stylesheets and tests reach into the markup, the public surface includes what the component renders and what it exposes to CSS.

Five surfaces are covered:

  1. The exported JavaScript and TypeScript API, which comprises components, hooks, functions, types, and their prop names, types and defaults.
  2. The rendered DOM, which covers element structure, nesting order and the roles and accessible names it produces.
  3. The data-* attributes listed in Data attributes, and the conditions under which they appear.
  4. The CSS custom properties listed in CSS variables, and the semantic token names they implement.
  5. The registry contract, which covers item names, the URLs they are served from, and the files a given item installs.

Removing or renaming anything on that list, or changing the condition that produces it, is a major version. This is a stronger promise than most component libraries make, and it is the one that matters in practice: an upgrade that keeps every prop and silently rewrites the markup will break somebody's stylesheet, somebody's end-to-end test and somebody's screen-reader expectations at the same time.

What is not covered

  • Exact token values. A colour may be adjusted; what is promised is the token's meaning and the contrast floor it must meet. A value change that fixes a measured contrast failure is a patch. A value change that alters what a token means is breaking, and is a rename in disguise.
  • Unprefixed internal CSS variables. Anything not beginning --opsin- is internal, may change in a patch, and is documented nowhere for that reason.
  • Class names. The utility classes a component renders are an implementation detail. Select on data-* attributes; that is what they are for.
  • Anything marked @internal in the type declarations. It is suppressed from the generated API pages so that it is never mistaken for surface.
  • The documentation site's own chrome. Its lyra, shadcn and fumadocs variables share a cascade with ours on this site and are not part of the system. ADR 0004 explains why they look different on purpose.
  • A shipped component's API, which may change in any release with a changelog entry rather than a deprecation cycle. See Release phases.

Before 1.0

This project is at 0.x, and 0.x means what it says:

  • A minor release (0.4.00.5.0) may contain a breaking change. It will be in the changelog, with a migration note, and on the deprecations list if something was removed.
  • A patch release (0.4.10.4.2) never contains a deliberate breaking change to the five surfaces above.
  • Token names are not yet stable. Token meanings are, and have been since they were written.

Pin an exact version if a broken upgrade would be expensive for you. The state of the system page names what is currently most likely to move.

Deprecation, and how you find out

Nothing is removed without passing through deprecation first.

It is marked

The component or export is marked deprecated, its documentation page states the replacement and the reason, and it appears on Deprecations with the version that deprecated it and the version that will remove it.

It keeps working

For at least one minor release before 1.0, and for a full major after it. A deprecated thing that stops working is a removal that lied about being a deprecation.

It is removed

In the release named when it was deprecated, with a changelog entry. A change that a reader cannot act on from a paragraph gets a written guide as well, and in practice that means one of four things: the rendered DOM moved, so stylesheets and end-to-end selectors move with it; a data-* attribute or a CSS custom property was renamed, which no type checker will catch; a clinical contract changed, which is a reason to re-run a safety review rather than a code edit; or the upgrade cannot be done in one step and the intermediate state has to be described. No release has needed one yet.

How a change reaches you

In the code you own. This is the weak link today. An out-of-date copy of a component in your repository does not announce itself the way a version range in a lockfile does. Nothing in the file records where it came from, so the only record of which version you copied is the one you write down yourself: run shadcn add in its own commit naming the item and the date, and put your own changes in the next one. Upgrades and diffs sets that habit out in full.

In the documentation. There is one published copy of every page and it describes the current version. The site keeps no per-version archive, so there is no older copy to land on and nothing that warns you that you have. Changing the URL of a page is a breaking change for programs, because the .md twins and /llms.txt are addresses that a crawler or an agent has already stored. What this site does about that today is nothing: next.config.mjs defines rewrites and no redirects, so a page that moves or goes away returns a 404 rather than sending you on. Five addresses have already gone that way, removed when the release phases were cut to three: /registry/version-stamps, /project/proposals, /project/migration, /project/decisions/0008-considered-components-resolve and /project/decisions/0022-considered-roster-built-out. Their subjects are now at Upgrades and diffs, Proposing a component, this page, and ADR 0023 for both records. Until a redirect map exists, re-resolve a stored address against /llms.txt when it stops answering.

Not written yet.

Add an async redirects() block to apps/www/next.config.mjs covering those five paths, and make adding one a step in any change that moves a page, so that the paragraph above can promise a redirect instead of describing a 404.

In the changelog. Every release has an entry, written as prose rather than as a list of commit subjects, and the breaking parts are at the top rather than at the bottom.

Edit this page

Last read through against the system on 2026-09-02. Due for review every 12 months; expiry is reported by pnpm run check:freshness.

On this page