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:
- The exported JavaScript and TypeScript API, which comprises components, hooks, functions, types, and their prop names, types and defaults.
- The rendered DOM, which covers element structure, nesting order and the roles and accessible names it produces.
- The
data-*attributes listed in Data attributes, and the conditions under which they appear. - The CSS custom properties listed in CSS variables, and the semantic token names they implement.
- 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
@internalin 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
shippedcomponent'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.0→0.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.1→0.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.
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.
Last read through against the system on 2026-09-02. Due for review every 12 months; expiry is reported by pnpm run check:freshness.