[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"topic-tailwindcss":3,"$f2v3tzw7dti6zg":-1,"articles-feed-\u002Ftopics\u002Ftailwindcss-1-tailwindcss-":8},{"articleCount":4,"color":5,"id":6,"name":7,"slug":7},1,"#10b981","01a0e2bd-04d1-7481-a0b7-6f06337485a1","tailwindcss",{"items":9,"page":4,"pageSize":41,"totalCount":4},[10],{"content":11,"createdAt":12,"id":13,"image":14,"isAffiliate":15,"isPublished":16,"publishedAt":17,"slug":18,"sourceId":19,"sourceName":20,"sourceType":21,"summary":22,"title":23,"updatedAt":24,"url":25,"urlHash":26,"tags":27},"Tailwind is awesome. I like having the styling next to the markup. I can read a component and understand its layout without jumping between files. But there’s a huge gap between using Tailwind and automatically verifying that code follows a design system. This becomes especially obvious when AI agents write the UI. An agent can produce valid Tailwind while ignoring the tokens and component variants already in your project. Telling it to follow the design system gives it instructions. It also needs a way to check its work. Take this button: &lt;BaseButton class=\"bg-red-500\"&gt;Ship it&lt;\u002FBaseButton&gt; bg-red-500 is valid Tailwind. The app builds. But the button has its own variants and theme colors. Someone just painted over them. Tailwind generates CSS for valid utilities. It doesn’t decide whether a caller should change this button’s background. You can enforce conventions with custom lint rules and other tools. That work existed before this package. But the policy doesn’t come from Tailwind itself. That’s the motivation behind @shadcn\u002Flint. Shadcn built it for AI agents that write UI. When an agent breaks a design-system rule, the lint error explains the violation, suggests an existing token or variant, and points to the relevant code. The agent can make the correction and run the check again. That’s what I like about the idea. It turns design-system contracts into checks both developers and agents can run. Components own their appearance. Callers use their variants and the classes your policy permits. I built a small Vue todo lab to try the idea. Then I deliberately broke the styling to see what the linter caught. Try breaking the design system The rendered button is on the left. Its Vue source is on the right. Switch to After below them to see the component, source, and lint findings change together. These findings come from actual ESLint runs against the Vue examples. The previews render Vue with a scoped stylesheet matching the lab’s theme. The widgets display captured results; they don’t run ESLint in your browser. The first example produces two findings. no-raw-colors rejects the palette color. no-restyle rejects changing the shared button’s color at the call site. The repair uses the button’s existing API: &lt;BaseButton variant=\"danger\"&gt;Ship it&lt;\u002FBaseButton&gt; The nice thing is that the message knows which variants exist. In the lab, it lists primary, secondary, ghost, and danger. It also points to src\u002Fcomponents\u002Fui\u002FBaseButton.vue. That’s useful feedback for a developer. It’s also useful for a coding agent. The next action is concrete, and the agent can run the same check after its edit. A component contract is more than a color rule The no-restyle rule checks recognized design-system components. Token rules also check plain elements. That distinction matters. Changing a paragraph to text-danger can fix its color violation. Changing the button to class=\"bg-danger\" still overrides its appearance. A semantic token doesn’t give every caller permission to use it everywhere. On a plain paragraph, the fix is simpler. Replace the palette color with the role it serves. Here, text-red-500 becomes text-danger: The text still says the same thing. Its color now comes from the theme’s danger token. With allow: [\"layout\"], a caller can position a button: &lt;BaseButton class=\"mt-4 w-full\" variant=\"danger\"&gt; Delete completed tasks &lt;\u002FBaseButton&gt; The component still controls its padding, background, and radius. If the button needs another size, that belongs in its size API. Different components can have different contracts. A card’s content area might allow padding changes. A title might allow typography changes: \u002F\u002F Options for shadcn\u002Fno-restyle { allow: [\"layout\"], contracts: [ { pattern: \"^CardContent$\", allow: [\"layout\", \"spacing\"] }, { pattern: \"^CardTitle$\", allow: [\"layout\", \"typography\"] }, ], } Each contract replaces the allow list, so keep layout when you still want it. The no-restyle documentation explains matching and exceptions. I like this more than a blanket ban on class. The policy can reflect how the component is meant to work. What the linter reads The package analyzes source code. It reads component imports, variants, theme declarations, and supported class expressions. For unknown classes, it asks the installed Tailwind 4 whether they generate CSS. If Tailwind or the theme cannot load, no-unknown-classes warns and falls back to weaker grammar checks. Resolve that warning before trusting a clean run. The fallback documentation explains the limits. The loop is simple: run the checks, read the allowed alternatives, edit the source, and run the checks again. There’s no AI service judging the design. The feedback comes from static analysis and the configured rules. The package supports React, Vue, and Svelte. You don’t need shadcn\u002Fui components. Your own UI directory and component APIs can provide the contract. I’m using @shadcn\u002Flint 0.2.0 with Tailwind 4 here. Vue templates need ESLint with vue-eslint-parser. Oxlint exposes the script blocks to this plugin, but not the templates. The Vue setup documentation covers that difference. If a project already uses Oxlint, keep it for its existing checks. Add ESLint for the Vue template policy. Add the rules to a Vue project This setup assumes an existing Vue project with Tailwind 4. The plugin requires Node.js 20.19 or later and supports ESLint 9.30 or later. Use a Node version supported by your installed ESLint release. Check the ESLint prerequisites. pnpm add -D @shadcn\u002Flint eslint @typescript-eslint\u002Fparser vue-eslint-parser Merge the following into your flat config. If eslint-plugin-vue already configures the Vue parser, preserve that setup and add the plugin and rules there. \u002F\u002F eslint.config.mjs export default defineConfig([ { files: [\"**\u002F*.vue\"], languageOptions: { parser: vueParser, parserOptions: { parser: tsParser }, }, plugins: { shadcn }, rules: { \"shadcn\u002Fno-restyle\": [\"error\", { allow: [\"layout\"], componentImports: [\"^@\u002Fcomponents\u002Fui(?:\u002F|$)\"], }], \"shadcn\u002Fno-raw-colors\": \"error\", \"shadcn\u002Fno-arbitrary-values\": \"error\", \"shadcn\u002Fno-unknown-classes\": \"error\", }, }, { files: [\"src\u002Fcomponents\u002Fui\u002F**\u002F*.vue\"], rules: { \"shadcn\u002Fno-restyle\": \"off\" }, }, ]) The last block lets shared components define their appearance. It keeps the token rules enabled inside those components. Adjust the path to your UI directory. The plugin discovers the theme through components.json, or a stylesheet that imports Tailwind when that file is absent. Check discovery when the project has multiple themes. Class helpers can also live in TypeScript files. Add another config block with files: [\"**\u002F*.ts\"], tsParser, the plugin, and the rules you want there. The Vue block doesn’t cover those files. For an initial run: pnpm exec eslint src Add that command to the project’s lint scripts and CI. If you roll out rules as warnings, use --max-warnings 0 when you’re ready to make warnings fail CI. I would start with a small set of shared components. Fix the findings, then expand coverage. A thousand warnings that everyone ignores won’t enforce anything. The limits are real A clean lint result doesn’t prove that the page looks good. The linter doesn’t inspect screenshots, measure contrast, or check whether a dialog fits a narrow screen. Browser tests and visual review still have work to do. It also has source-analysis limits. Vue &lt;style&gt; blocks and stylesheet declarations need separate CSS checks. Imported class values and runtime expressions can fall outside what it can resolve. require-static-classes can report unreadable class values on recognized components. It’s an additional rule, and the four-rule config above doesn’t enable it. New theme tokens and disabled rules also deserve review. An agent can silence a finding by adding a token for every exception. That passes a token check while making the design system worse. And don’t expect --fix to design the interface. Choosing a new variant, accepting a different radius, or changing a semantic role needs judgment. Make the tokens worth enforcing The linter needs a useful policy. I would start with semantic names that describe a role, such as primary, danger, surface, and muted-foreground. A call site using bg-indigo-600 describes a color choice. A shared component using bg-primary describes what the color does. Try the theme controls below. Switch Indigo to Teal, then switch Light to Dark. The semantic components update together. The hardcoded examples keep their fixed colors. The demo uses CSS variables to show the difference. A token change reaches every component that references that token. The raw values don’t have that connection. In Tailwind 4, the mapping can look like this: \u002F* src\u002Fstyles.css *\u002F @import \"tailwindcss\"; :root { --primary: #4338ca; --primary-foreground: #ffffff; --surface: #ffffff; --foreground: #18181b; } .dark { --primary: #a5b4fc; --primary-foreground: #1e1b4b; --surface: #18181b; --foreground: #fafafa; } @theme inline { --color-primary: var(--primary); --color-primary-foreground: var(--primary-foreground); --color-surface: var(--surface); --color-foreground: var(--foreground); } Now bg-primary text-primary-foreground reads the active variables. Put .dark on the relevant ancestor to switch those values. Explicit dark: utilities need the project’s corresponding dark-mode configuration. Tailwind’s theme documentation explains @theme inline. Its dark-mode guide covers selector-based switching. Hardcoded values belong in token definitions. That’s where the design chooses actual colors. At call sites, use the roles those definitions expose. Keep foreground and background tokens paired. When a background changes, review its text color too. Check contrast in both themes and in hover, focus, and disabled states. Keep sizes and radii intentional as well. Shared components should expose a small set of sizes. Avoid adding --special-page-13px just to satisfy a lint rule. This last example replaces rounded-[13px] with rounded-xl. The visual difference is small, but the radius now uses a named step: That changes the radius from 13px to 12px in this theme. A nearby value isn’t automatically the right value. The linter suggests a scale step; I still need to decide whether it fits. There’s a subtle catch with spacing. Tailwind 4 supports dynamic numeric spacing utilities. Removing square brackets doesn’t create a finite spacing scale. The no-arbitrary-values documentation even shows p-[13px] becoming p-3.25 with the default spacing unit. Both represent 13px. If your policy permits only specific spacing steps, enforce that restriction explicitly. no-arbitrary-values alone doesn’t do it. Keep conditional classes complete and visible in source: const statusClasses = { error: \"bg-danger text-danger-foreground\", ready: \"bg-primary text-primary-foreground\", } as const Avoid constructing bg-${color}-500. Complete class strings help Tailwind detect utilities and give static analysis something concrete to inspect. See Tailwind’s source detection rules. For a stricter project, @theme { --color-*: initial; } removes the default color namespace. Declare your own colors afterward. Review existing usages before doing that. Resetting --color-* leaves --spacing intact. My rule would be simple. Variants own component appearance. Semantic tokens connect components to the theme. Consumers get the layout changes their contracts allow. Start enforcing that policy on a small area. Review exceptions and new tokens as design decisions. Then a lint failure can point to a real violation, and a passing check means something specific.","2026-09-27T12:00:17.768Z","01a0e2bc-c766-7305-a420-8a49418a4256","https:\u002F\u002Falexop.dev\u002Fposts\u002Fenforce-tailwind-css-design-tokens-in-vue-with-shadcn-lint\u002Findex.png",false,true,"2026-09-27T00:00:00.000Z","enforce-tailwind-css-design-tokens-in-vue-with-shadcnlint","019d70dd-e3e7-76db-84a4-87b896dea004","alexop.dev","rss","The article discusses the use of @shadcn\u002Flint to enforce Tailwind CSS design tokens in Vue applications, addressing the gap between using Tailwind and ensuring adherence to design systems. It highlights how this linting tool can help both developers and AI agents maintain design consistency by providing feedback on violations and suggesting corrections. A practical example is provided, demonstrating the integration of the linter within a Vue project to enhance styling practices.","Enforce Tailwind CSS Design Tokens in Vue with @shadcn\u002Flint","2026-09-27T12:00:33.462Z","https:\u002F\u002Falexop.dev\u002Fposts\u002Fenforce-tailwind-design-system-shadcn-lint\u002F","5939069459d013ae7dd0d752cb057a7cf9a6a8541ecf4727458be5714d7cb5a5",[28,31,34,35,38],{"color":5,"id":29,"name":30,"slug":30},"019d6bd8-c9a8-7783-bd22-03145b355427","vue",{"color":5,"id":32,"name":33,"slug":33},"019f374d-0cc4-71ff-b906-ebaec8b0c343","eslint",{"color":5,"id":6,"name":7,"slug":7},{"color":5,"id":36,"name":37,"slug":37},"01a0e2bd-04d7-7267-9d82-95ae773fbb20","linting",{"color":5,"id":39,"name":40,"slug":40},"01a0e2bd-04de-77ec-bfa6-e802944f2dff","design-tokens",20]