Styling
className, data attributes, CSS variables and the style prop are the four override hooks, listed in their fixed order of preference, with the rule for choosing.
The short version
NOT IMPLEMENTED. This component does not exist in any released version of opsinjs. There is no package to install, no module to import and no props interface to generate code against. Everything on this page is a specification of intended behaviour and may change without notice. Do not write code against it.
This component is not built yet
PlannedRoadmapWhat “planned” means
opsinjs components will expose exactly four styling hooks. Reach for them in this order, and stop at the first one that works:
- A token. Change the value, not the component. If several components look wrong the same way, a token is wrong.
className. Per instance, per part, merged withcn()so your utilities win over the defaults.- A CSS variable on a selector you own. For values a component reads at
runtime, such as a track height or an accent, scoped to a container rather
than dumped on
:root. - The
styleprop. Last. It wins over everything, which is why it is last: it also wins over the theme, over dark mode, and over the reader's preferences.
You own the source, so editing the file is always a fifth option. That is a feature of registry distribution, not a failure of the first four, but it does mean you own the upgrade.
How it works
className and cn
Every part accepts className. It is merged with the component's own classes
using cn() from lib/utils, which is clsx for conditional logic and
tailwind-merge for conflict resolution:
import { cn } from "@/lib/utils"
cn("px-4 py-2", isWide && "px-8")
// → "py-2 px-8". tailwind-merge resolves the px conflict in your favourWithout tailwind-merge the two px-* utilities would both be present and the
winner would be whichever CSS rule the stylesheet happened to order last, which
is not something you want to depend on.
className on a compound component applies to the part you put it on, not to
the subtree. This is the most common surprise:
// Applies to the root element only.
<ResultCard className="max-w-sm">
{/* Applies to the value part only. */}
<ResultCard.Value className="tabular-nums" />
</ResultCard>Data attributes
Every stateful part exposes its state as a data-* attribute, so you can style
a state without a class toggle, a JavaScript branch or a render prop:
.my-pill[data-status="urgent"] { /* … */ }
.my-sheet[data-open] { /* … */ }The full vocabulary is Data attributes, and it is generated into Reference rather than listed by hand.
CSS variables
Where a component needs a value at runtime, such as a computed track width or an accent that has to interpolate, it reads a CSS custom property. Override it on a selector you own:
.dashboard {
--opsin-range-bar-track-height: 0.5rem;
}Scope it. Setting a component variable on :root changes every instance in the
application, including the ones you have not seen.
The style prop
Available, unrestricted, and a last resort. It produces an inline declaration that beats your stylesheet, the theme and the dark-mode block, which means it also survives changes you wanted it not to survive.
Do this
- Fix it at the token layer when more than one component is affected. One
token change is auditable; fourteen
classNameoverrides are not. - Style state through
data-*, not through a class you toggle. The component already knows its state and has told the DOM; a parallel class is a second source of truth that will eventually disagree. - Scope CSS variables to a container.
- Use
cn()for every merge, including in your own wrappers, so utility conflicts resolve in the caller's favour consistently. - Put
classNamelast in your prop spread if you are forwarding props, or the caller's value will be overwritten by the default.
Not this
- Do not use
!importantto win a specificity fight. Under Tailwind v4 the layer order usually means you did not need to; if you genuinely did, the component's class list is wrong and that is a bug worth reporting. - Do not hardcode a colour.
bg-[#e11d48]bypasses the theme, both modes, the contrast floor and the two-axis rule in one keystroke. This is the invariantopsinjs/no-raw-colorexists to catch. - Do not style by descendant selector into a component's internals.
.card > div > spanwill break on any structural change and you will not find out until the layout collapses. Use the part's ownclassNameor its data attributes. - Do not use a category token to express status, or a status token to express identity. This is the single most important rule in the system, it is not a styling preference, and it is explained in The two colour axes.
- Do not reach for
stylebecause it was quicker. It is quicker once and slower every time after.
Gotchas
tailwind-mergeneeds to know about your custom utilities. If you have extended the theme with new scales, conflicting classes in those scales may not resolve as you expect until it is configured for them.- Class sorting silently no-ops without
tailwindStylesheetin.prettierrc. Under Tailwind v4 the Prettier plugin needs to be told where the stylesheet is, and without it your classes stay in whatever order they were typed with no error. - Order in
globals.cssdecides who wins. The lyra block, the opsinjs token layer and@layer baseare ordered deliberately; moving an@importchanges cascade outcomes without any diagnostic. See Tailwind v4. - A CSS variable set on
:rootin a docs page leaks into previews. Scope it, or the component preview two sections down will change too. - Arbitrary values disable the theme, not just the token.
text-[13px]opts that instance out of the type scale and out of Dynamic Type; see Dynamic type.
Related
- Data attributes is the state vocabulary you style against, and the one to read next.
- Token architecture explains why the first hook is a token, and which tier you are allowed to touch.
- Adding your own tokens covers extending the system in a way that survives an upgrade.
- ESLint plugin has the rules that catch a raw colour and a mixed axis before review does.
Handbook
The mechanics layer is how you change things. It is kept deliberately separate from what a token means and from the generated list of every one.
Data attributes
The shared state-attribute vocabulary. Where it comes from, which attributes are Base UI's and which are opsinjs's, and why it is a versioned contract.