@pandacss/eslint-plugin
ESLint rules for Panda CSS that read your actual config. They know your tokens, recipes, patterns, and utilities — so they catch a typo'd token, a deprecated recipe, or a hardcoded color that should be a token.
There's no JSON to generate first. The plugin loads your panda.config.ts once and lints against it.
Beta. ESLint v9 flat config only. Most rules are report-only; a few add fixes/suggestions (noted below).
Install
pnpm add -D @pandacss/eslint-plugin
You need ESLint 9 or later.
Setup
Add the recommended config to eslint.config.mjs. Loading your Panda config is async, and ESLint
supports a flat config that resolves to a Promise,
so await it (top-level await works in flat config):
import panda from '@pandacss/eslint-plugin'
export default [await panda.configs.recommended({ configPath: './panda.config.ts' })]
That's it. The rules now run against your project's tokens, recipes, and utilities.
Top-level await works in eslint.config.mjs (or .js with "type": "module") and in eslint.config.ts (jiti ≥2 /
Node ≥22.13 / Deno / Bun). In a CommonJS eslint.config.cjs, export a Promise instead — ESLint resolves it:
module.exports = (async () => {
const panda = (await import('@pandacss/eslint-plugin')).default
return [await panda.configs.recommended({ configPath: './panda.config.ts' })]
})()
The recommended config scopes itself to JS/TS/JSX files and does not set a parser — use your project's existing
parser. Most TS setups already have one via typescript-eslint, and framework projects
via their ESLint preset (eslint-plugin-vue, astro-eslint-parser, etc.). Panda's rules read source text directly, so
any parser that produces a Program works.
To lint framework files (.vue, .svelte, .astro), opt them in via files once their parser is configured:
await panda.configs.recommended({ configPath: './panda.config.ts', files: ['**/*.{ts,tsx,vue}'] })
Enabling opt-in rules / changing severities
recommended binds every rule under the @pandacss plugin, so add opt-in rules (or tweak severities) in your own
rules block after it:
export default [
await panda.configs.recommended({ configPath: './panda.config.ts' }),
{ rules: { '@pandacss/consistent-property-style': ['error', { style: 'shorthand' }] } },
]
Monorepos
Call recommended once per Panda config and scope each to its package via files:
export default [
{
...(await panda.configs.recommended({ configPath: './packages/web/panda.config.ts' })),
files: ['packages/web/**'],
},
{
...(await panda.configs.recommended({ configPath: './packages/app/panda.config.ts' })),
files: ['packages/app/**'],
},
]
The Panda config is loaded once and cached; a long-running editor session won't pick up
panda.configedits until ESLint restarts.
oxlint
The same rules run under oxlint via its ESLint-compatible JS plugins (alpha — needs oxlint +
@oxlint/plugins). Point jsPlugins at the @pandacss/eslint-plugin/oxlint entry and enable the rules in
.oxlintrc.json:
{
"jsPlugins": ["@pandacss/eslint-plugin/oxlint"],
"rules": {
"@pandacss/no-invalid-nesting": "error",
"@pandacss/no-debug": "warn",
"@pandacss/prefer-token": ["warn", { "categories": ["colors"] }]
}
}
The entry auto-discovers panda.config.* from the working directory; set PANDA_CONFIG_PATH to point at a config in a
non-standard location.
Single config per run. Unlike the ESLint flat config (where you compose several recommended({ configPath })
entries), oxlint loads a JS plugin once and binds one Panda project. For a custom path or a monorepo with several
configs, write a tiny local plugin instead of using the entry, hardcoding the configPath:
// oxlint-panda.mjs
import { createPandaPlugin } from '@pandacss/eslint-plugin'
const { rules } = await createPandaPlugin({ configPath: './panda.config.ts' })
export default { meta: { name: '@pandacss' }, rules }
{ "jsPlugins": ["./oxlint-panda.mjs"], "rules": { "@pandacss/no-invalid-nesting": "error" } }
oxlint.config.ts (via defineConfig from oxlint, Node ≥22.18) works the same way — plugins are still referenced by
path, so the setup is identical.
Rules
These rules are on in recommended:
no-invalid-token-paths(error) — a token reference that doesn't exist, e.g.token('colors.ghost').no-invalid-nesting(error) — a nested selector missing&(':hover'instead of'&:hover'), which Panda silently ignores. Suggests prefixing&.file-not-included(error) — a file uses Panda but sits outside your configinclude, so its styles never get generated.no-deprecated(warn) — use of a deprecated token, utility, recipe, or pattern. If you setdeprecated: 'use X instead'in config, that message shows up in the lint error.prefer-token(warn) — a raw value where a token exists, with the token to use. Inrecommendedit's scoped to colors (the oldno-hardcoded-color); widen it withcategories(see below).no-debug(warn) — a leftoverdebug: true.extraction-diagnostics(warn) — parse or extraction problems Panda hit in the file.
The rest are off by default. Turn them on per project:
no-important—!importantin styles.no-margin-properties— margin props; nudges you towardgapand layout patterns.no-physical-properties— physical props that have logical equivalents (left→insetInlineStart).no-shorthand-longhand-mix— a shorthand and one of its longhands in the same block (margin+marginLeft); the longhand wins regardless of source order. Takesignore(groups to allow).consistent-property-style— enforce one property style: Panda shorthand aliases (ml) or longhand canonical names (marginLeft). Autofixable. Takesstyle: 'shorthand' | 'longhand'(defaultlonghand) andignore.prefer-text-style— a style object setting two or more typography properties that should be onetextStyletoken.no-primitive-token— a primitive/core token where a semantic token exists for that category. Takescategoriesandallow.
Enable an opt-in rule like any other:
import panda from '@pandacss/eslint-plugin'
export default [
await panda.configs.recommended({ configPath: './panda.config.ts' }),
{ rules: { '@pandacss/no-important': 'error' } },
]
Options
no-deprecated takes a kinds option to narrow what it checks:
{ rules: { '@pandacss/no-deprecated': ['warn', { kinds: ['tokens', 'recipes'] }] } }
Valid kinds: tokens, utilities, recipes, patterns. All are checked by default.
prefer-token takes categories (which token categories to enforce; defaults to all) and allow (raw values to
permit):
{ rules: { '@pandacss/prefer-token': ['warn', { categories: ['colors', 'spacing'], allow: ['transparent'] }] } }
It lists the tokens that carry the value and offers each as an editor quick-fix, so you pick — semantic and primitive
both shown (semantic first), themed tokens marked (themed). It matches equivalent forms (#FFF == #ffffff, 16px
== 1rem):
Hardcoded colors value "#f00". Matching tokens: fg.error, red.500.
💡 Use the token "fg.error"
💡 Use the token "red.500"
Quick-fixes apply to each offending leaf — flat literals (color: '#f00'), values nested in conditions
(color: { base: '#f00' }), and responsive-array elements (color: ['#f00', ...]). Coverage spans css(), style
props, and recipe styles in cva() / sva() / styled('div', { ... }) (base, variants, compoundVariants).
no-primitive-token takes categories (which token categories to enforce; defaults to categories with semantic tokens)
and allow (primitive token paths to permit):
{ rules: { '@pandacss/no-primitive-token': ['error', { categories: ['colors'], allow: ['red.500'] }] } }
This complements prefer-token: prefer-token maps hardcoded CSS values to tokens; no-primitive-token maps primitive
tokens to semantic tokens. Categories without semantic tokens are skipped, so spacing primitives are allowed unless you
define semantic spacing tokens.
Migrating from no-hardcoded-color
no-hardcoded-color is now prefer-token scoped to colors. Replace it with:
{ rules: { '@pandacss/prefer-token': ['warn', { categories: ['colors'] }] } }
recommended already does this, so if you use configs.recommended there's nothing to change.
Settings
The plugin finds your config in this order:
settings.panda.configPathsettings['@pandacss/configPath'](migration alias)- the nearest Panda config to the file being linted
export default [panda.configs.recommended(), { settings: { panda: { configPath: './panda.config.ts' } } }]
How it works
The plugin builds one Panda compiler per config and reuses it across files. Rules don't re-parse — they ask the compiler
what each file means to Panda (which calls are css, which tag is a recipe, which value resolved to a token) and report
on the ESLint nodes. Config loading happens once, up front, so the rule visitors stay synchronous and fast.