Skip to content

Shadcn Just Fixed Tailwind's Biggest Problem

Read the Portuguese translation →

Author: Better Stack · Watch on YouTube

Video authored by the channel shown, not by IANIA; all rights belong to the creator. Transcript and summary generated automatically; the original-language content is preserved.

Original language: English

Original video: Shadcn Just Fixed Tailwind’s Biggest Problem · Channel: Better Stack · 06:21 · Watch on YouTube

Topics: AI agents, Software engineering with AI, AI developer tools

Shadcn’s new Tailwind linter is designed to help enforce design systems, especially when AI agents generate code. It provides six rules that catch issues such as unauthorized restyling, raw colors, arbitrary values, inline styles, unknown classes, and dynamic classes. Its errors can suggest valid component variants and theme tokens, giving agents specific guidance without requiring them to load full design documentation. The linter is customizable, but it cannot inspect plain CSS or decide whether newly added design tokens belong in the system, and it currently supports only Oxlint and ESLint.

  • The linter aims to prevent AI-generated Tailwind code from violating a project’s design system.
  • In Tailwind, class names are strings, so TypeScript alone does not prevent invalid styling choices.
  • The linter is available as a plugin for Oxlint or ESLint and works with Tailwind V4 projects; Shadcn UI is not required.
  • The no-restyle rule allows layout changes while preventing components from being restyled with properties such as padding, color, typography, shape, effects, or motion.
  • The linter can read component variants from a CVA setup and use them to offer relevant fixes.
  • Other rules prevent raw colors, arbitrary values, inline styles, unknown classes, and dynamic classes.
  • Contracts let teams customize rules for specific components, while custom messages and global notes can provide agents with additional guidance.
  • The linter cannot enforce rules in plain CSS or determine whether newly added theme tokens belong in the design system.
  • A useful component API with defined variants is a prerequisite for the linter to suggest appropriate options.
  • There is no Biome plugin for the linter yet.
Start Chapter Summary
00:00 Why Tailwind needs design-system enforcement The video introduces the linter and describes how AI-generated Tailwind styles can violate design systems, then presents results showing the linter reduced violations in tested tasks.
01:37 The no-restyle rule The video explains how the first rule allows layout changes but blocks unauthorized restyling, using component variants to provide useful errors and suggestions.
03:12 The other rules and design-system contracts The remaining rules address arbitrary values, colors, inline styles, unknown classes, and dynamic classes, while contracts and custom messages tailor enforcement to components.
04:16 Customization, limitations, and availability The video covers contract configuration and the linter’s limitations, including its inability to inspect plain CSS or judge new tokens, then notes its current Oxlint and ESLint support.

It’s an agent-first linter built for Tailwind design systems. — 00:00

pages are allowed to place a component with things like margin, width, flex, and hidden, but they’re not allowed to restyle them. — 01:37

the error actually tells you what the nearest theme token is — 03:44

The linter can only check the rules. It can’t decide whether a new orange actually belongs in your design system. — 05:49

There is no biome plugin for this yet — 06:21

Tailwind V4, shadcn/ui, shadcn/ui, Oxlint, ESLint, StyleX, CVA, Biome, sistema de design, contratos

00:00 Shadcn just released a linter to try and solve the biggest problem with Tailwind today, design systems. With the rise of AI agents, Tailwind has never had a great way to enforce one. So, you may have noticed AI sometimes adds in its own styles we didn’t really want it to. It’s a large reason why people have turned to alternatives like StyleX, but now Shadcn has a solution for that. It’s an agent-first linter built for Tailwind design systems. So, let’s just jump in and see what this does. Let’s start out with what this linter actually does. In Tailwind, a class name

00:33 can be a string. TypeScript doesn’t enforce anything beyond that, and that means that you can put a padding for override on a button that controls its own padding. You could also add a random color like background pink 500 to a component that already uses a themed color, or just use some arbitrary padding amount like 13 pixels when your design system already has a spacing scale that goes from 12 to 16. Currently, none of these will cause any errors in your code base, and they’ll only be picked up by you actually reviewing the code or just adding a load of rules to a markdown file that another agent can review for you. But, markdown is not great at enforcing things. It is

01:06 not as strict as a linter can be. Shadcn actually ran the numbers on this. He set up eight tasks that attempt the agent to go off the system with prompts like add a delete button, the design calls for it to be pink with pill corners, build a stats card matching this mock exactly, 13 pixels padding, 10 pixel border radius, or make the pricing card really pop so it stands out. You can see here that every single model produced a lot of design system violations when running these tasks, but when they used the linter, it dropped down to zero. So, now that we know the linter actually works, how do we use it? Well, it’s a plugin

01:37 for Oxlint or ESLint, and it will work on any Tailwind V4 project. You don’t have to be using Shadcn UI. We can start out with one rule called no restyle, but allowing layout. And this essentially means that pages are allowed to place a component with things like margin, width, flex, and hidden, but they’re not allowed to restyle them. So, they can’t add padding, color, typography, shape, effects, or motion. If they do, it’ll throw an error like this. P4 is not allowed on button. Button owns its spacing. Use a size default or margin here or a gap on the parent for space around it. Add a size in the component

02:09 only if the design system explicitly calls for one. The cool thing about these error messages here is something like the size list isn’t actually defined by the linter. It’s actually reading my CVA setup in the button component itself. So, if I added an extra size, the error would pick that up as an option. The same goes for basically any tailwind feature that can restyle. So, if we try to add a color here, it would tell us we can only use the ones defined in the component. And obviously, the benefit of this is that you get really nice error messages and an agent can read them and understand how to fix them. This can actually save you on token usage as well because it

02:39 doesn’t have to load in your full design markdown files to understand what’s correct. The linter can just give it a very specific clue. Now, that’s the basic usage of our very first rule, no restyle, but there is loads of customization options to make it suit your design system, but we’ll come back to that in a minute. First, let’s take a look at the other five rules as there’s actually six in total. No raw colors is a rule to stop you typing in things like background pink 500 or using a color that doesn’t exist or is misspelled. It has to be one that was designed in the theme. And again, it will help the agent out by telling them directly what is allowed. This even works on SVGs. So, if

03:12 you try to use a hard-coded fill on a path, you get an error saying use current color with a text color class. Next up, we have no arbitrary values. This does what it says on the tin. Something like padding with 13 pixels in the square brackets here gets hard codes an off-token value. Use P3.25 instead. It’s the same value on the scale. Or something like rounded 10 pixels in those square brackets gets use rounded large instead because it can actually read my radius token and know that’s the value that equals 10 pixels. One of the coolest examples of this rule actually understanding your design system is if

03:44 you try and set a random background color, the error actually tells you what the nearest theme token is, which is super nice context for your agent to have. The rest of the rules are pretty self-explanatory. There’s a rule to stop inline styles, another to stop unknown classes being used, and one to enforce static classes. This one’s interesting because the linter can’t actually see what this class is going to be since it’s a dynamic in that template literal. So, this rule encourages you to change the way that you handle these so the linter can still do its job effectively. Those are our six linter rules though, but as mentioned earlier, there’s lots of customizability to make this actually

04:16 suit your design system. This is where you would use contracts. These are per component rules matched by a regex on the component name. So, in this example, I said we can change a card title’s typography, but not its font family or weight, and card content can change the spacing, but not the typography. So, now text large on this title passes with no errors, and padding six on the content passes, but trying to change the title’s weight still gives us an error as expected. Another great option in these settings is custom messages. Every rule and type of rule takes a message with placeholders that are filled from your

04:47 actual code. So, on my button contract here, I’ve set the message to be set to be set width on the parent container, not on button, for layout, and also button owns its padding, use a button size with a template for the sizes. Those will be filled in for me from my own design system. You can also set a global note, so every finding will have this message attached at the end. This is useful to give some extra hints or context to the agent, maybe telling it where your documentation or guidelines are. If you already have a design system in place, this might be a little bit of extra work to actually get started with using this linter as you have to configure it for your design system, but

05:19 it seems flexible enough that it can handle most systems, and I’m sure an AI agent can help you get started. The final thing I want to talk about is what this linter can’t do. It can’t see plain CSS, so if you have a raw color in your global CSS file or an apply, it can’t enforce rules around that. It also can’t trace parent selectors down to the child. It only follows class values for one hop within a file, and a brand new theme token is on system by definition, so an agent that actually adds color or paint to your design system to get around the rule will still pass. This does mean that you still need to review

05:49 the tokens and the variants that the agent is adding. The linter can only check the rules. It can’t decide whether a new orange actually belongs in your design system. It’s still on you to check, but hopefully this linter should take out some of that extra work. Something else to think about if you’re considering using this is is your component API actually strict enough to enforce against? If your button simply takes any class name and has no variants, there’s nothing for this linter to suggest. So, the prerequisite is that you actually have a design system in place and use actual variants, which to be fair, if you’re using shadcn/ui, you already have. So, most

06:21 people will probably be fine. The final downside is that for now this is only available in oxlint and eslint. There is no biome plugin for this yet, but this is an open issue on their GitHub. So, hopefully we see this soon. So, that’s the new shadcn linter. It’s six rules that help you enforce your design system, which is really helpful in an era where AI is writing most of the code. I’m curious if this fixes any of the problems that you’ve run into or if you’re still considering switching to something like StyleX. Let me know in the comments down below. Oh yeah, like subscribe. And as always, see you in the next one. >> [snorts]