Stylelint plugin
The proposed CSS rule set catches hardcoded colours, tier violations, and a status token applied to a category surface.
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
stylelint-plugin-opsinjs is specified but not built. CSS needs its own plugin
rather than an ESLint rule because most of the violations that matter happen in
a stylesheet, not in JSX. They surface in an override file, a theme, a copied
component's CSS.
// stylelint.config.mjs is proposed and not yet publishable
export default {
plugins: ["stylelint-plugin-opsinjs"],
extends: ["stylelint-plugin-opsinjs/recommended"],
}How it works
Proposed rules
opsinjs/no-raw-color is an error-level rule.
Any colour literal in a declaration whose property affects colour: hex, rgb(),
hsl(), oklch() and named colours. The exception is the token definition
files themselves, where the literals are the point.
Why: the same reason as in JSX, with a wider blast radius. A stylesheet rule
applies to every instance.
opsinjs/token-tier is an error-level rule.
Flags a component-level custom property assigned a primitive value directly,
skipping the semantic tier. Tiers are defined in
Token architecture.
Why: the three-tier structure is what makes a theme swap possible; a component
reading a primitive is not themeable.
opsinjs/no-axis-crossing is an error-level rule.
Flags a --opsin-status-* value assigned to a --opsin-category-* property, or
the reverse, and flags a status token used in a declaration on a selector that
also carries [data-category] in a way that changes the same visual channel.
Why: the two colour axes, enforced where
the mixing actually happens.
opsinjs/no-contrast-floor-violation is a warn-level rule.
Where both sides of a foreground/background pair are opsinjs tokens, checks the
pair against the published floor using the same APCA implementation as
check-contrast.mts.
Why: it moves a CI failure to the editor. It cannot check a pair involving a
value it does not know, which is why it warns rather than errors.
opsinjs/no-transition-on-layout is a warn-level rule.
Flags transition and animation on width, height, top, left,
margin, padding.
Why: they do not composite. See
Motion in practice.
opsinjs/require-logical-properties is a warn-level rule.
Flags margin-left, padding-right, left, right, text-align: left|right
in favour of their logical equivalents.
Why: right-to-left support is cheap to keep and expensive to retrofit. See
Internationalisation.
opsinjs/no-global-component-variable is a warn-level rule.
Flags a --opsin-<component>-* property set on :root, html or body.
Why: it changes every instance in the application, including the ones the
author has never seen. See Styling.
opsinjs/no-opacity-for-emphasis is a warn-level rule.
Flags an opacity below a threshold on an element carrying text.
Why: reducing opacity to make something look secondary reduces contrast
against the background, which is the most common accidental contrast failure and
the one that hits stale values hardest. See
The contrast floor.
Do this
- Run it on your overrides and your theme files, which is where the violations concentrate.
- Keep the token definition files in the documented exception list rather than disabling the rule for them ad hoc.
- Fix
no-opacity-for-emphasiswith a token, not with a slightly higher opacity.
Not this
- Do not exclude your theme file to make
no-raw-colorpass. The theme is where the literals belong, and the exception should say so explicitly. - Do not disable
no-axis-crossing. If it fires, either the token is wrong or the design is. Both are worth stopping for. - Do not run it on
globals.csswholesale in this repository. That file is in.prettierignorefor related reasons, and its lyra block is intentionally preserved byte-for-byte from the shadcn CLI so it can be diffed against a fresh init.
Gotchas
- CSS custom properties are untyped. The plugin can only reason about names it recognises; a project-specific property assigned a status value is invisible.
- Contrast checking needs both sides. A token against
inherit, against an image, or against a translucent material cannot be evaluated statically. That is what the contrast floor and its scrim rules exist for. - Tailwind generates CSS the linter never sees. Utility classes are compiled from your markup, so a hardcoded colour in an arbitrary value is an ESLint problem, not a Stylelint one.
oklch()literals are legitimate in exactly one place. That place is the generated token layer, and they are illegitimate everywhere else, which makes the exception list load-bearing.
Related
- Tooling covers the three-layer enforcement model.
- ESLint plugin covers the same invariants in JSX.
- Token architecture has the tiers
token-tierenforces. - Validating your theme has the CI check that catches what a linter cannot.
ESLint plugin
The proposed rule set for JSX and TypeScript covers raw colours, mixed axes, banned words, and the component contracts a reviewer should not have to remember.
Migrating from shadcn/ui
The token map, the component equivalences and the API translations. It ends with an honest list of what opsinjs has no equivalent for.