Rules

September 17, 2026 ยท View on GitHub

Enable each rule at warn or error. Start with the README setup, then add the checks your project needs.

RuleWhat it checks
no-restyleClasses passed to design-system components.
no-raw-colorsPalette colors, undeclared tokens, and raw SVG colors.
no-arbitrary-valuesArbitrary values such as p-[13px].
no-inline-stylesInline styles and <style> elements.
no-unknown-classesClasses the project's Tailwind cannot generate.
require-static-classesUnreadable class values on components.

Turn no-restyle off inside your component directory so components can style themselves. The setup includes this override. When you enable no-arbitrary-values and require-static-classes, turn them off there too. See Add more rules for that override.

Shared options

The policy

Every rule accepts message. Every rule except require-static-classes also accepts allow, deny, and contracts:

OptionWhat it does
allowAllows matching classes or properties through this rule.
denyRemoves matches from allow. Without allow, permits everything else.
contractsSets a policy for matching components.
messageReplaces the rule's error text.

For no-restyle, these options decide which classes a component accepts. For token and class-existence rules, they define exceptions to the check. For no-inline-styles, they apply to CSS property names.

"shadcn/no-restyle": ["error", {
  allow: ["layout"],
  deny: ["w-*"],
}]

Margin passes. Width does not. Appearance classes are still reported.

An omitted allow and an empty allow are different when deny is present:

ConfigurationPolicy
Neither allow nor denyNo policy exceptions.
allow: [...]Only matching classes or properties are exempt.
allow: []No policy exceptions.
deny: [...] without allowEverything except these matches is exempt.
deny: [] without allowEverything is exempt.

The policy only affects what the rule checks. For example, deny: ["bg-primary"] does not make a declared token a raw color. Likewise, class exceptions do not bypass SVG attribute checks, and property exceptions do not bypass <style> or dynamic-style checks.

Contracts

A contract's pattern is a regex on the resolved component name. ^Button$ matches only Button; Button also matches IconButton.

A contract replaces the keys it writes and inherits the rest from the top-level rule. If several contracts match, only the last one applies; it does not inherit from earlier contracts.

"shadcn/no-raw-colors": ["error", {
  allow: ["*-amber-*"],
  deny: ["bg-amber-500"],
  contracts: [
    {
      pattern: "^Badge$",
      allow: ["*-amber-500"],
      deny: [],
    },
  ],
}]

Outside Badge, amber classes pass except bg-amber-500. On Badge, only amber-500 classes are exempt. deny: [] clears the inherited denial, so bg-amber-500 passes there. Declared theme colors pass in both cases.

For no-inline-styles, contracts match the JSX component name as written, without resolving wrappers. Lowercase elements use the top-level policy.

Class rules accept these entries in allow and deny:

EntryExampleWhat it matches
Categorylayout, spacing, colorClasses in that category.
Class groupp, px, bg-color, roundedValues in that group.
Class or patternp-4, p-*, md:p-*An exact class or wildcard pattern.

Groups are separate. p does not include px or py; rounded does not include rounded-t-*. An entry that is both a group and a class matches both: flex covers flex and flex-1.

Entries without : match the base class, ignoring variants, important markers, negative prefixes, and opacity modifiers. p-* matches md:!p-4; bg-primary matches bg-primary/50. Entries containing : match the full class. [margin:*] matches [margin:1rem], but not md:[margin:1rem] or [margin:1rem]!.

These are class-name checks, not checks for equivalent CSS effects. w-* does not cover inline-full or [width:100%]. Allowing p-* also allows p-[13px] through no-restyle; no-arbitrary-values still checks that value.

Invalid regexes and misspellings of a category or class group such as spacig produce a configuration error on line 1 and pause the rule for that file. Other unknown names, such as prose from a Tailwind plugin or a custom btn, may be real classes, so they produce a warning and match a class named exactly that. no-unknown-classes accepts unknown entries because they may name external classes.

Recognition

Every rule except no-inline-styles accepts these options as arrays of strings:

OptionWhat it does
componentImportsRecognizes component imports using regex patterns.
ignoreImportsExcludes matching imports from component recognition.
mergeFunctionsAdds functions whose arguments contain classes.
variantFunctionsAdds functions whose object values contain classes.

Import ignores take precedence over recognition. Function lists extend the built-ins: cn, cx, clsx, cva, tv, twMerge, twJoin, and classNames for class arguments; cva and tv for variant objects.

Set shared defaults through settings.shadcn:

settings: {
  shadcn: {
    ui: "@/ds",
    mergeFunctions: ["mergeClasses"],
  },
}

ui is an import prefix: @/ds matches @/ds and @/ds/button, but not @/dsx. Use an array for multiple prefixes. It always applies alongside componentImports.

Shared recognition settings accept a string or an array. A rule's own option takes precedence over the matching shared setting. See Settings for all settings and a monorepo example.

Your own words

Set message to replace a rule's error text:

"shadcn/no-raw-colors": ["error", {
  message: 'Use a theme color for "{{className}}". See {{file}}.',
}]

Every finding provides these placeholders, empty when they do not apply:

PlaceholderValue
{{className}}The class, or an SVG attribute such as fill="#f00".
{{property}}The inline CSS property.
{{component}}The component name.
{{suggestions}}Suggested tokens, scale values, or a spelling correction.
{{file}}The relevant theme or component file.

no-restyle also accepts a message object with category keys:

"shadcn/no-restyle": ["error", {
  allow: ["layout"],
  message: {
    spacing: "Use a {{component}} size: {{sizes|none defined}}.",
    default: "Use a {{component}} variant: {{variants|none defined}}.",
  },
}]

Category keys are layout, color, typography, spacing, shape, effects, and motion. default covers the other categories, including unclassified names. A contract can provide its own message in the same format.

Additional no-restyle placeholders:

PlaceholderValue
{{category}}The class category, including layout or unclassified.
{{variants}}Comma-separated variant names, or empty.
{{wrapper}}The forwarding component name, or empty.
{{sizes}}Size names on spacing findings, except explicit deny findings.
{{around}}Where spacing can go instead, on spacing findings.
{{entries}}The relevant allow or deny entries.

{{around}} names the places the contracts accept, for example margin here, gap on the parent, or spacing on <CardContent>.

Other rules provide these placeholders on the findings that use them:

PlaceholderValue
{{tokens}}Declared color names on no-raw-colors and no-arbitrary-values color findings, up to 12.
{{suggestion}}The corrected class on no-raw-colors and no-unknown-classes spelling findings. On SVG attribute findings, the nearest token name.
{{replacement}}The equivalent scale or token class on no-arbitrary-values findings, such as p-3.25.
{{attribute}}The SVG attribute name on no-raw-colors attribute findings, such as fill.
{{value}}That attribute's value, such as #f00.

{{suggestions}} already contains the {{suggestion}} or {{replacement}} value when a finding provides one.

Use {{variants|none defined}} to supply a fallback for an empty value. Unknown or unavailable placeholders stay literal, including their fallback. Likely placeholder typos produce a warning. Messages are limited to 500 characters.

The linter first checks the contract's category message, then its default. If neither applies, it checks the rule's message, then the built-in guidance. This lets a contract customize one category and keep the rule's guidance for everything else.

To append a note to every rule's findings, including custom messages:

settings: {
  shadcn: {
    note: "See docs/design-rules.md for design rules and approved exceptions.",
  },
}

Categories

Under no-restyle with allow: ["layout"]:

CategoryExamplesResult
layoutmt-4, w-full, hidden, absolute, flex-1Allowed.
colorbg-primary, text-red-500, border-borderReported.
typographytext-sm, font-bold, leading-none, truncateReported.
spacingp-4, gap-2, space-x-4Reported.
shaperounded-lg, border, ring-2, outline-noneReported.
effectsshadow-sm, opacity-50, blur, backdrop-blurReported.
motionanimate-pulse, transition, duration-200Reported.
unclassifiedflex-cols, a custom class unknown to the grammarReported.

Margin, transforms, and text alignment are layout. Padding and gap are spacing. Tailwind markers such as group, group/name, and peer also pass with layout allowed.

unclassified is a reported category, not an allowance you can configure. Allow a custom class by name, for example allow: ["layout", "tap-target"]. A class your own CSS declares with @utility, or as a plain selector, is still unclassified: the rule cannot read what it changes. It is reported in its own words, which do not call it a misspelling. You can open a category for a component with a contract:

{ pattern: "^CardContent$", allow: ["layout", "spacing"] }

The complete mapping is in GROUP_CATEGORY. It is an internal constant, not a package export.