RoadieRoadie
  • Home
  • Brand
  • Foundations
  • Content
  • Tokens
  • Components
  • Charts
  • Widgets
  • Appearance
HomeBrandFoundations
Appearance

Home

  • Installation
  • Building with AI
  • Philosophy
  • Vue integration
  • Linting
  • Changelog

Linting

Docs updated
24 Oct 2026

@oztix/roadie-lint-plugin checks the Roadie conventions a rule can be sure of: Phosphor icon imports, sizes, and weights, colour classes, arbitrary radius and z-index values, Tailwind classes not in their canonical form, a Card without Card.Content, and tests that sleep, assert class strings, or check in jsdom what only a browser can. Calls that need judgement, such as which emphasis a surface takes, stay with review. Lint with oxlint. ESLint runs the same rules, with the same rule IDs, for an app that can't move to oxlint yet.

Install

pnpm add -D @oztix/roadie-lint-plugin oxlint

It needs oxlint 1.88 or later. Both oxlint and ESLint are optional peer dependencies, so an app installs only the one it lints with. If your app can't move to oxlint yet covers ESLint.

Before 0.2.0, the package was @oztix/roadie-eslint-plugin. That name is deprecated: its last release, 0.1.2, re-exports this package and warns once when it loads, outside production. To move, change the name in package.json and in your config. The rules and their IDs are the same.

Turn on the rules

In .oxlintrc.json, extend the recommended preset by its path. Oxlint's extends takes a file path, not a package name.

{
"extends": [
"./node_modules/@oztix/roadie-lint-plugin/oxlint/recommended.json"
]
}

In oxlint.config.ts, import the preset and extend it.

import { defineConfig } from 'oxlint'
import recommended from '@oztix/roadie-lint-plugin/oxlint/recommended.json' with {
type: 'json'
}

The preset turns every rule in the Rules table on as an error and loads the plugin under the name roadie. To choose rules yourself, load it in jsPlugins with that name and name each rule.

{
"jsPlugins": [
{ "name": "roadie", "specifier": "@oztix/roadie-lint-plugin" }
],
"rules": {
"roadie/no-dark-variant": "error",

Without name, oxlint names the plugin after its package, so every rule ID would start with @oztix/roadie-lint-plugin/ instead of roadie/.

Rules

In a config, each rule takes the roadie/ prefix, such as roadie/no-dark-variant.

RuleFailsFix
phosphor-icon-suffixA Phosphor import by its bare name, such as HeartImport HeartIcon
phosphor-icon-size-propThe size prop on a Phosphor iconA size-* class
phosphor-icon-weightA Phosphor icon with no weight, or one other than bold, fill, or duotoneweight='bold'
no-dark-variantA dark: variantA semantic colour utility
no-hex-colour-classA hex colour, such as bg-[#fff]A semantic colour utility
no-arbitrary-z-indexAn arbitrary z-index, such as z-[5]A named z tier, or z-1 inside a component
no-arbitrary-radiusAn arbitrary radius, such as rounded-[10px]A named radius tier, such as rounded-xl
canonical-classA Tailwind class not in its canonical form, such as p-[16px]Its canonical form, such as p-4, which --fix writes
card-contentA Card with no Card.Content among its partsPut the body, or the title and description, in Card.Content

Iconography says which weight goes where, Colors why .dark needs no variant, Elevation lists the z tiers, and Shape the radius tiers.

The class rules read className, any prop ending in ClassName, and the arguments of cn, clsx, cva, cx, tv, and twMerge. They also read a const those name, such as variants in cn(variants[size]), when it holds a string, array, or object. They follow one constant, not a constant built from another, and don't read CSS, so @apply goes unchecked. roadie/no-arbitrary-radius also fails a raw length, such as [--card-radius:10px], in a variable that a rounded-(--card-radius) class reads.

Two icon rules skip a case on purpose. roadie/phosphor-icon-weight skips an icon passed to Navigator.Item, including one in a fallback such as icon={icons[href] ?? <HouseIcon />}, because Navigator sets its own weight. An icon built elsewhere, such as in a map of icons, still fails, so disable the rule around it and say why. roadie/phosphor-icon-size-prop allows size='100%', which fills an SVG around the icon, such as a QR code's centre mark.

roadie/canonical-class asks Tailwind for each class's canonical form under Roadie's theme, as Tailwind's editor extension suggests, so rounded-[8px] becomes rounded-lg and data-[pressed]:flex becomes data-pressed:flex. It keeps z-1 rather than -z-hide, which reads as a different tier. The answer comes from your app's own tailwindcss, so it can change when you upgrade: from 4.2, start-0 becomes inset-s-0. It finds Roadie's stylesheet from the linted file, so it skips a file outside an app with @oztix/roadie-core installed. It skips every file in an app with no tailwindcss, or one before 4.1.15. Loading Tailwind adds about a second to the first file a lint run checks.

roadie/card-content judges a Card imported by name from @oztix/roadie-components, not through a namespace import, and only when it can see every part. A card whose children include your own component, an expression such as a map, or a fragment passes, since any of those might render Card.Content. Card warns in development when it renders without one, which catches the rest.

Testing

The testing preset catches tests that can't be trusted. jsdom has no layout engine and doesn't evaluate CSS, so a jsdom test of layout or CSS passes whatever the page looks like. So does a test that checks the class string a variants function returns. A fixed sleep makes a test slow, and flaky when the wait runs short.

The preset turns its rules on for *.test.* files. Tests that run in a real browser skip the jsdom rules: *.browser.test.* files, such as tests in Vitest's browser mode, *.spec.* files, which Playwright runs, and any file in an e2e folder. Those keep no-fixed-sleep, and a *.test.* file among them keeps no-cva-output-assertion too. If your jsdom tests are named *.spec.*, turn the jsdom rules and no-cva-output-assertion on for those files in a config after the preset.

In .oxlintrc.json, extend it after the recommended preset.

{
"extends": [
"./node_modules/@oztix/roadie-lint-plugin/oxlint/recommended.json",
"./node_modules/@oztix/roadie-lint-plugin/oxlint/testing.json"
]
}

In oxlint.config.ts, import @oztix/roadie-lint-plugin/oxlint/testing.json and add it to extends after recommended.

In ESLint, spread configs.testing into your config array, as If your app can't move to oxlint yet shows.

RuleFailsFix
no-css-class-in-jsdomA calc(), container query, or breakpoint class in toHaveClass, such as md:gridMeasure it in a browser test
no-css-source-in-jsdomReading CSS source, with a ?raw import or readFileSyncSet up the condition in a browser test and read the computed style
no-cva-output-assertionAn assertion on the class string a variants function returns, such as buttonVariants()Render it and assert what a user sees
no-fixed-sleepA fixed wait, such as setTimeout inside a promise, page.waitForTimeout, or timers/promisesWait on a condition with expect.poll or waitFor
no-layout-fake-in-jsdomA faked layout read, such as getBoundingClientRect or offsetWidth, or a matchMedia fake that answers a viewport queryA browser test at a real width

A matchMedia fake that answers only a preference, such as prefers-reduced-motion, passes, because it fakes a setting, not layout. A test that checks a generated file against the CSS it comes from is a fair reason to read CSS source. Turn roadie/no-css-source-in-jsdom off for that file in a config after the preset, and say why in a comment.

Icons without an import

The Phosphor rules find icons by their import. Code that gets icons another way, such as a live code editor with icons in scope, names them in settings.roadie.phosphorIcons.

{
"settings": {
"roadie": { "phosphorIcons": ["HeartIcon", "StarIcon"] }
}
}

settings goes at the top level of the config. Oxlint fails to load a config with settings inside overrides.

If your app can't move to oxlint yet

Lint with ESLint 9 or 10 on flat config (eslint.config.js) until it can. The rules and their IDs are the same, so moving later changes only the config. Install the plugin without oxlint.

pnpm add -D @oztix/roadie-lint-plugin

Add the recommended config to your config array.

import roadie from '@oztix/roadie-lint-plugin'
export default [
// Your other configs
roadie.configs.recommended
]

It turns every rule in the Rules table on as an error for JavaScript and TypeScript files, and turns on JSX parsing. TypeScript files still need the parser your config gives them, such as typescript-eslint's.

To choose rules yourself, register the plugin as roadie and name each rule.

import roadie from '@oztix/roadie-lint-plugin'
export default [
{
files: ['src/**/*.{ts,tsx}'],
plugins: { roadie },

Spread the testing preset into the array after the recommended config, and put the settings from Icons without an import in a config of its own.

import roadie from '@oztix/roadie-lint-plugin'
export default [
roadie.configs.recommended,
...roadie.configs.testing,
{

An app on a legacy .eslintrc can't load the plugin in ESLint. Move it to oxlint, or to flat config.

Previous page← Vue integrationNext pageChangelog →