opsinjs
IntroductionStart here

Browser support

The tested floor, and the per-feature contract for what happens in a browser that lacks a progressive enhancement.

Overview

opsinjs uses several CSS features that are not evenly supported, and it uses them on purpose: the material ladder, the squircle geometry, wide-gamut colour and spring-shaped easing are what make the product theme feel like a native health app rather than a web dashboard. None of them is allowed to be load-bearing.

This page states two separate things, and they are separate on purpose:

  • The floor is the oldest engine opsinjs is tested against, which is a policy decision the project makes and can be held to.
  • The enhancement contract covers, for each optional feature, what it does, and precisely what a browser that lacks it gets instead. This is the part that matters, because it is the part a reader experiences.

The support matrix is not published yet

Sixty components are implemented and none of them has been tested against a browser matrix, so a per-feature version table on this page would be remembered rather than measured. The matrix below renders from generated data and stays empty until a test run produces some. The contract is a design decision, and it is stated in full. It sets out what degrades to what.

The tested floor

Safari and iOS Safari 16.4, and the current and previous major versions of Chrome, Edge and Firefox.

That is a commitment about testing, not a claim about behaviour: below the floor, pages will very often still work, and we will not knowingly break them, but we do not test and will not treat a defect there as a release blocker.

The floor is set by the reader, not by the developer. Consumer health apps are read overwhelmingly on phones, and a meaningful share of those phones belong to people who do not update them. Those are often the older and less well readers whom a health product least wants to exclude. That argues for a conservative floor. It also argues against making the floor so old that the system has to abandon the visual language that makes it feel trustworthy, which is why everything below the floor is handled by enhancement rather than by exclusion.

FeatureYour browserWhat happens without it
backdrop-filterChecking…Translucent material rungs fall back to their opaque colour. Layout is unchanged and the contrast floor is unchanged, because the fallback is the value the floor was measured against.
corner-shape (squircle)Checking…A plain rounded rectangle at the same radius. A taste difference, never a functional one.
Display-P3 colourChecking…The sRGB ramp. The escalation only raises chroma and keeps hue and lightness identical. Every measured contrast figure therefore holds in both gamuts.
linear() easingChecking…The browser's default easing. A spring is one custom property holding one linear() value, so there is no cubic-bezier approximation behind it: the declaration that substitutes it is invalid at computed-value time and the timing function is lost. Identical duration and identical end state; the curve is what goes.
:has()Checking…The status matrix filter and a handful of docs-only affordances stop filtering and show every row. Nothing in the product theme depends on it.

Tested live with CSS.supports() in the browser you are reading this in. Nothing here is a claim about a browser matrix somebody maintained by hand.

Not written yet.

Not one of the sixty components has been tested against any browser, at the floor or above it. The floor above is a commitment about what will be tested, and the table renders from generated data that does not exist yet. Until a run produces some, treat every row of the enhancement contract below as a design decision rather than an observation.

The enhancement contract

Read this as a promise about degradation. Every row is implemented with an @supports query or a media query, never with user-agent detection. linear() easing is the one exception: no @supports guard is available for it, so it relies instead on CSS's own rule for a value an engine cannot compute. Each fallback is a designed state rather than an accident, and the one that is a bare browser default says so below rather than dressing itself up as a curve we chose.

Backdrop blur and the material ladder

Six of the system's surface rungs are defined partly by a backdrop filter. Without it, each rung falls back to an opaque surface whose colour is derived to hit the same measured contrast against the same content beneath. The ladder stays ordered and the floor is still met; what is lost is the sense of depth.

The same fallback path is used, deliberately, for prefers-reduced-transparency, so the opaque route is exercised by real users on every engine rather than being an untested branch. See Reduced transparency.

Squircle geometry with corner-shape

The product theme uses continuous corner curvature rather than a circular radius, because it is what the platforms these readers use every day look like. Where corner-shape is unavailable, surfaces fall back to a plain border-radius at the same token value. Nothing reflows, nothing changes size, and no information depends on the difference. See Shape.

Wide-gamut colour in Display P3

Colours are authored in OKLCH and emitted with an sRGB fallback first and a color-gamut: p3 enhancement second. A display or engine without wide gamut gets the sRGB value, which is not a compromise: the sRGB value is the one the contrast floor is measured against, so conformance never depends on a wide-gamut screen. The P3 version is a saturation improvement on hardware that can show it, and the category and status hues stay distinguishable in both. See Colour and gamut.

Spring easing as linear()

Motion tokens are springs, exported as linear() easing functions with many stops. Each spring is a single custom property, and there is no cubic-bezier approximation behind it: a custom property holds linear(…) even where the engine cannot compute it, so the cascade has nothing to fall back to. What happens instead is that the declaration using it is transition-timing-function: var(--opsin-ease-spring-…), and it is invalid at computed-value time. The property takes its initial value, and the move runs on the browser's default easing. Durations are separate tokens and are identical either way, so nothing in a sequence gets out of step and no transition is lost; what is lost is the shape of the curve. The matrix above tests the feature in the browser you are reading this in, which is the only honest answer here. The version table is not published yet.

Under prefers-reduced-motion: reduce, both paths are replaced by the reduced variant. That variant is not "no animation", but the specific degradation documented per component. See Reduced motion.

Everything else

Container queries, :has(), @property, subgrid and text-wrap: balance are used where they help and never where their absence changes meaning. If one of them is missing, a layout is slightly less refined; no value, status or control disappears.

Verify it worked

The check that matters is not "does it look right in my browser". It is "does the fallback path still meet the floor", and you can test that without an old device:

  1. Turn on Reduce Transparency in your operating system's accessibility settings. Every material rung should become opaque and stay readable. This exercises the same code path as a browser with no backdrop filter.
  2. Turn on Reduce Motion. Every transition should take its documented reduced form rather than simply vanishing.
  3. Force sRGB. In a browser devtools rendering panel, emulate an sRGB display. Categories must remain distinguishable from one another and statuses from each other.
  4. Disable one feature at a time in devtools rather than testing on an old phone. It is faster and it isolates the cause.

Troubleshooting

A surface is opaque when you expected blur. Check prefers-reduced-transparency before you check the browser. It is the most common cause and it is working as designed.

Corners look circular rather than squircular. Expected without corner-shape. It is cosmetic, and no measurement, target size or contrast result depends on it.

Colours look duller than the documentation. You are on an sRGB display, or the page is being rendered in an sRGB context. The sRGB values are the conformant ones; the documentation is showing you the wide-gamut enhancement.

Motion runs, but the springs feel generic. linear() was not understood, so the timing function was dropped at substitution and the browser's default easing is running. There is no cubic-bezier approximation waiting behind it. Durations and end states are preserved; only the shape of the curve differs.

Something genuinely breaks below the floor. Report it. We will not necessarily fix it, and we will tell you which of those two it is rather than leaving the issue open.

Next

  • Framework support is the other half of the support question: React version, server components and bundlers.
  • Materials says what the ladder is and why the opaque fallback is a designed state rather than a compromise.
  • Springs as tokens is where the linear() values come from.

On this page