Shadcn Just Fixed Tailwind's Biggest Problem

BBetter Stack
Computing/SoftwareInternet Technology

Transcript

00:00:00ShadCN just released a linter to try and solve the biggest problem with Tailwind today,
00:00:04design systems. With the rise of AI agents, Tailwind has never had a great way to enforce one,
00:00:09so you may have noticed AI sometimes adds in its own styles where you didn't really want it to.
00:00:13It's a large reason why people have turned to alternatives like StarLex,
00:00:17but now ShadCN has a solution for that. It's an agent-first linter built for Tailwind design
00:00:21systems, so let's just jump in and see what this does.
00:00:29Let's start out with what this linter actually does. In Tailwind, a class name can be a string,
00:00:34TypeScript doesn't enforce anything beyond that, and that means that you can put a padding for
00:00:38override on a button that controls its own padding. You could also add a random color like background
00:00:43pink 500 to a component that already uses a themed color, or just use some arbitrary padding amount
00:00:48like 13 pixels when your design system already has a spacing scale that goes from 12 to 16.
00:00:53Currently, none of these will cause any errors in your code base, and they'll only be picked up by
00:00:58you actually reviewing the code, or just adding a load of rules to a markdown file that another
00:01:02agent can review for you. But markdown is not great at enforcing things. It is not as strict
00:01:07as a linter can be. ShadCN actually ran the numbers on this. He set up 8 tasks that tempt the
00:01:12agent to go off the system, with prompts like add a delete button, the design calls for it to be pink
00:01:16with pill corners, build a stats card matching this mock exactly, 13 pixels padding, 10 pixel border
00:01:22radius, or make the pricing card really pop so it stands out. You can see here that every single
00:01:27model produced a lot of design system violations when running these tasks, but when they used the
00:01:31linter, it dropped down to zero. So now that we know the linter actually works, how do we use it?
00:01:36Well, it's a plugin for Oxlint or ESLint, and it will work on any Tailwind V4 project. You don't have to be
00:01:41using ShadCN UI. We can start out with one rule called no restyle, but allowing layout. And this
00:01:47essentially means that pages are allowed to place a component with things like margin, width, flex,
00:01:51and hidden, but they're not allowed to restyle them. So they can't add padding, color, typography,
00:01:56shape, effects, or motion. If they do, it will throw an error like this. P4 is not allowed on button,
00:02:02button owns its spacing, use a size default or margin here, or a gap on the parent for space around it.
00:02:07Add a size in the component only if the design system explicitly calls for one.
00:02:11The cool thing about these error messages here is something like the size list isn't actually
00:02:15defined by the linter. It's actually reading my CVA setup in the button component itself.
00:02:19So if I added an extra size, the error would pick that up as an option. The same goes for basically
00:02:24any Tailwind feature that can restyle. So if we try to add a color here, it would tell us we can only
00:02:29use the ones defined in the component. And obviously the benefit of this is that you get really nice
00:02:33error messages, and an agent can read them and understand how to fix them. This can actually save
00:02:37you on token usage as well, because it doesn't have to load in your full design markdown file,
00:02:41to understand what's correct. The linter can just give it a very specific clue.
00:02:45Now that's the basic usage of our very first rule, no restyle, but there is loads of customization
00:02:50options to make it suit your design system, but we'll come back to that in a minute. First let's
00:02:54take a look at the other five rules, as there's actually six in total. No raw colors is a rule
00:02:58to stop you typing in things like background pink 500, or using a color that doesn't exist or is
00:03:03misspelt. It has to be one that was designed in a theme, and again it will help the agent out by
00:03:08telling them directly what is allowed. This even works on SVGs, so if you try to use a hardcoded fill
00:03:13on a path, you get an error saying use current color with a text color class. Next up we have no
00:03:18arbitrary values, this does what it says on the tin. Something like padding with 13 pixels in the square
00:03:24brackets here, gets hardcodes and off token value, use p3.25 instead, it's the same value on the scale.
00:03:30Or something like rounded 10 pixels in those square brackets gets use rounded large instead,
00:03:35because it can actually read my radius token and know that's the value that equals 10 pixels.
00:03:40One of the coolest examples of this rule actually understanding your design system is if you try
00:03:44and set a random background color, the error actually tells you what the nearest theme token is,
00:03:49which is super nice context for your agent to have. The rest of the rules are pretty self-explanatory.
00:03:54There's a rule to stop inline styles, another to stop unknown classes being used,
00:03:57and one to enforce static classes. This one's interesting because the linter can't actually
00:04:02see what this class is going to be, since it's a dynamic in that template literal,
00:04:06so this rule encourages you to change the way that you handle these,
00:04:09so the linter can still do its job effectively. Those are our six linter rules though,
00:04:13but as mentioned earlier, there's lots of customizability to make this actually suit
00:04:16your design system. This is where you'd use contracts. These are per component rules
00:04:21matched by a regex on the component name, so in this example I've said we can change a card
00:04:25title's typography, but not its font family or weight, and card content can change the spacing,
00:04:30but not the typography. So now text large on this title passes with no errors, and padding six on
00:04:36the content passes, but trying to change the title's weight still gives us an error as expected.
00:04:41Another great option in these settings is custom messages. Every rule and type of rule takes a
00:04:45message with placeholders that are filled from your actual code, so on my button contract here,
00:04:50I've set the message to be set width on the parent container, not on button, for layout, and also
00:04:55button owns its padding, use a button size with a template for the sizes. Those will be filled in
00:04:59for me from my own design system. You can also set a global note, so every finding will have this
00:05:04message attached at the end. This is useful to give some extra hints or context to the agent,
00:05:08maybe telling it where your documentation or guidelines are. If you already have a design
00:05:12system in place, this might be a little bit of extra work to actually get started with using this
00:05:16linter, as you have to configure it for your design system, but it seems flexible enough that it can
00:05:20handle most design systems, and I'm sure an AI agent can help you get started. The final thing I want
00:05:25to talk about is what this linter can't do. It can't see plain CSS, so if you have a raw color in your
00:05:30global CSS file or an apply, it can't enforce rules around that. It also can't trace parent selectors
00:05:36down to the child, only follows class values for one hop within a file, and a brand new theme token
00:05:40is on system by definition, so an agent that actually adds color hopping to your design system to get
00:05:46around the rule will still pass. This does mean that you still need to review the tokens and the
00:05:50variants that the agent is adding. The linter can only check the rules, it can't decide whether a
00:05:54new orange actually belongs in your design system. It's still on you to check, but hopefully this
00:05:59linter should take out some of that extra work. Something else to think about if you're considering
00:06:03using this is, is your component API actually strict enough to enforce against? If your button
00:06:08simply takes any class name and has no variants, there's nothing for this linter to suggest.
00:06:13So the prerequisite is that you actually have a design system in place and use actual variants,
00:06:18which to be fair, if you're using ShadCN UI, you already have, so most people will probably be
00:06:22fine. The final downside is that for now this is only available in Oxlint and ESLint, there is no
00:06:27biome plugin for this yet, but this is an open issue on the GitHub, so hopefully we see this soon.
00:06:31So that's the new ShadCN Linter, it's six rules that help you enforce your design system,
00:06:36which is really helpful in an era where AI is writing most of the code. I'm curious if this fixes any of the
00:06:41problems that you've run into, or if you're still considering switching to something like StarLX,
00:06:45let me know in the comments down below, or yeah, then subscribe, and as always, see you in the next one.

Key Takeaway

ShadCN's 6-rule agent-first linter for Tailwind CSS V4 uses direct CVA code inspection to enforce design system boundaries and drop AI-generated styling violations to zero.

Highlights

  • AI models consistently violate Tailwind design systems across benchmark tests, but using the ShadCN linter reduces design violations to zero.

  • The linter integrates directly with Oxlint or ESLint and operates on any Tailwind CSS V4 project without requiring ShadCN UI components.

  • Reading component-level Class Variance Authority (CVA) configurations directly allows the linter to generate contextual error messages without separate markdown rule files.

  • Passing custom component regex rules via contracts prevents layout utility overrides from altering core properties like typography and color.

  • Linter enforcement saves LLM token usage by eliminating the need to inject complete design system guidelines into prompts.

Timeline

Design System Enforcement Breakdown in Tailwind CSS

  • Tailwind accepts standard string classes, preventing TypeScript from enforcing design system boundaries.
  • Prompt-temptation benchmarks across multiple AI models produced continuous design rule violations that drop to zero when using the linter.

Unrestricted utility class strings allow developer and AI contributions to introduce unintended style overrides, such as arbitrary 13-pixel padding or unapproved background colors. Relying on code reviews or instructions inside markdown files fails to enforce strict compliance. Benchmarks using tasks like generating pink pill buttons or 10-pixel border radius cards triggered persistent system deviations across every major LLM, whereas the linter completely blocked those non-compliant class assignments.

Preventing Component Restyling with 'no-restyle'

  • The 'no-restyle' rule permits external layout adjustments while banning overrides to internal component styling.
  • Error messaging inspects local CVA configurations directly to inform agents of allowable component variants.

Components retain internal ownership over padding, colors, typography, shapes, and motion while outer layouts control structural placement via margin, width, flex, or display properties. Attempting to force extra padding onto a standard button outputs precise feedback directing the agent toward valid variant keys extracted directly from Class Variance Authority definitions. Providing concrete runtime class suggestions inside terminal output eliminates the necessity of passing large design system documentation files inside context windows.

Enforcing System Color, Value, and Syntax Rules

  • Five additional rules restrict raw color inputs, arbitrary values, inline styles, unknown classes, and dynamic string construction.
  • The arbitrary value check maps hardcoded pixel measurements directly to existing theme tokens.

Rules like 'no-raw-colors' restrict class usage to defined theme variables across standard elements and vector path fills. Passing hardcoded units such as 13 pixels or 10-pixel border radiuses triggers errors recommending matching system tokens, such as p-3.25 or rounded-lg. Dynamic template literal strings are flagged to force explicit static strings that static analysis tools can evaluate successfully.

Contract Configurations and Agent Context Extensions

  • Contracts apply regular expression matching to component names for granular attribute rule definitions.
  • Custom error messages and global notes supply actionable instructions directly to automated agents.

Regex matching enables custom rule definitions per component type, such as allowing title font size updates while locking font weight and family. Standard rules accept template strings populated with real-time class names to instruct agents precisely where layout adjustments belong. Attaching global context notes to every linter error helps guide agents to official documentation links when violations occur.

Current Linter Scope, Constraints, and Integration Requirements

  • Analysis remains restricted to single-file class strings and cannot inspect raw CSS or dynamic theme additions.
  • Rule enforcement requires structured component APIs containing predefined variant options.

Static checking cannot trace raw CSS files, apply directive references, or track utility values passed across multiple files. Agents adding brand-new color tokens directly to theme configurations will bypass validation checks, requiring human review for new design system tokens. Support is currently restricted to ESLint and Oxlint plugins, with Biome integration remaining in development.

Community Posts

No posts yet. Be the first to write about this video!

Write about this video