Stop Button Restyling with Lint
Use @shadcn/lint to enforce Tailwind Button styling contracts.
Introduction
30 Second Summary
When several people or coding agents edit the same app, one button can quietly pick up new colours or padding. Soon, screens that should match start looking different.
In this project, you will build a small Tailwind v4 app around a reusable Button component. You will use @shadcn/lint with ESLint to enforce a contract for your design system.
What You'll Build
You will see lint catch an off-system Button change before the browser confirms that its baseline styling is restored.
By the end of this project, you'll have:
- A baseline Button you can open in the browser as the visual reference for every later check.
- A lint check that passes for approved layout classes while flagging off-system colours or padding.
- An agent-guided fix that turns a failing lint run into a clean pass.
- Secret Mission: Add the lint check to CI so every coding-agent change is checked against the same contract.
Are there any prerequisites?
You'll need Node with npm available on your computer. A coding agent is optional because you can apply the linter's suggested fix yourself.
Before We Start
A design-system linter can only protect your Button component if the local tools can run it. Missing tools would stop the feedback loop before the linter gets a chance to help.
Your local toolchain starts with Node.js. npm installs the packages those tools need.
Your editor gives the project a workspace. Your browser shows the visual result while your coding agent edits the same local folder.
In this step, get ready to:
- Verify the local JavaScript toolchain.
- Prepare your editing workspace.
- Confirm the full development environment is ready.
Check Node.js and npm
Node.js runs the development tools used throughout this project. A version response confirms that your terminal can find each command.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- Print both installed versions by running these commands:
node -v
npm -v
What Do These Checks Prove?
- The node command reports the installed Node.js version.
- The npm command reports the installed package manager version.
✔️ I see two version numbers
Good news. Two version numbers confirm that Node.js and npm are available from your terminal.
ⓧ One or both commands are not found
The Node.js installer includes npm. Installing the LTS release prepares both tools together.
- Visit the official Node.js download page.
- Choose the installer marked LTS for your operating system.
- Open the downloaded installer.
- Keep the default installation options.
- Complete the installation.
- Restart your terminal after the installation finishes.
- Check both tools again by running these commands:
node -v
npm -v
What Does This Check Do?
Each command asks the newly installed tool to report its version. This confirms that the restarted terminal can find both tools.
You should see one version number for Node.js followed by another version number for npm.
Still Missing a Version Number?
Restart your computer if restarting the terminal does not refresh the installation. Run the two checks again after signing back in.
If one command still cannot be found, reinstall the LTS release from the official Node.js download page.
Help me diagnose my Node.js installation.
Prepare your coding workspace
Your editor becomes the shared workspace for your files. The coding agent needs permission to read the local folder that the editor opens.
- Install a code editor from its official website if your computer does not already have one.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type the name of your code editor and press Enter to open it.
- Open your coding agent through its editor panel, desktop app, or terminal.
- Approve local folder access if the agent asks for permission.
- Confirm that the agent supports folders opened by your editor.
- Return to your operating system search bar.
- Type the name of your browser and press Enter to open it.
You should now have an editor window ready for local files. Your browser and coding agent should also be available.
Why Does Folder Access Matter?
The linter reports problems inside your project files. Folder access lets the coding agent read those messages in context before editing the affected component.
Run the final readiness check
A final terminal check confirms that the toolchain remains available after preparing the rest of your workspace.
Before you run the check, do you expect the terminal to print one version number or two?
- Return to the terminal from earlier.
- Print the two tool versions by running these commands:
node -v
npm -v
What Does Readiness Look Like?
The first line comes from Node.js. The second line comes from npm.
You should see two version numbers with no missing-command message. That output confirms your local JavaScript toolchain is ready.
Only Seeing One Version Number?
Restart the terminal before checking again. A terminal opened before installation may not detect the new tools.
If the problem continues, reinstall Node.js using the LTS option from the official download page.
Help me finish my environment check.
Your development environment is ready. Next up, you will create the Tailwind app and see the baseline Button in your browser.
Create the Baseline Button App
Design systems lose their value when the intended component appearance exists only in someone's memory. A visible baseline gives every later lint result something concrete to protect.
In this step, you will create a React app with Vite. Tailwind CSS v4 will define the Button's baseline appearance.
In this step, get ready to:
- Scaffold a React and TypeScript application on your Desktop.
- Configure Tailwind CSS v4 through Vite.
- Render a reusable Button with its baseline design-system appearance.
Scaffold the React app
Vite can generate the React and TypeScript files in one pass. Starting on your Desktop keeps the new repository easy to find.
- Move to your Desktop by running this command:
cd ~/Desktop
What does this command do?
The command changes the terminal's current location to your Desktop. Vite creates the project folder in that location.
The scaffold package may need to download before it runs. If npm asks for permission to continue, approve the request.
- Create the React and TypeScript application by running this command:
npm create vite@latest my-app -- --template react-ts
What does this command create?
- Create Vite generates a new repository named my-app.
- The react-ts template supplies React components with TypeScript support.
- The generated package.json contains the scripts that build and serve the app.
- Move the terminal into the generated repository by running this command:
cd my-app
Where are you now?
Your terminal now points at the my-app folder. Every project command in this walkthrough runs from this folder.
Dependency installation can take a minute while npm downloads the generated app's packages. A short pause here is expected.
- Install the generated dependencies plus the Tailwind integration by running these commands:
npm install
npm install tailwindcss @tailwindcss/vite
What did you install?
- The first command installs the dependencies declared by the Vite template.
- The second command adds Tailwind CSS plus its Vite plugin to package.json.
- The plugin lets Vite compile Tailwind utility classes while the app runs.
- Confirm the repository files exist by running this command:
ls
What should this check show?
You should see package.json plus the src folder in the output. These confirm that the application repository exists.
Your repository is ready. The dependency manifest now includes the packages required for the Tailwind styling pipeline.
Did the scaffold or installation fail?
- Check that the terminal is still inside the my-app folder before installing dependencies.
- Check your internet connection if npm cannot download a package.
- Remove an existing Desktop folder named my-app if the scaffold reports a naming conflict.
Help me diagnose my Vite scaffold or npm installation error.
Connect Tailwind CSS to Vite
Tailwind needs a build integration before its utility classes can affect the page. The Vite plugin performs that compilation whenever the project builds or runs.
- Use your code editor's folder picker to select the my-app folder on your Desktop.
- Select vite.config.ts in the editor's file explorer.
- Replace the file contents with this configuration:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
// Compile React and Tailwind CSS through Vite.
plugins: [react(), tailwindcss()],
})
What does this configuration do?
- The react() plugin compiles React components for the browser.
- The tailwindcss() plugin turns Tailwind classes into browser-ready CSS.
- The plugins array makes both transformations part of the same Vite build.
- Save vite.config.ts.
- Switch back to the terminal inside my-app.
- Check the Vite configuration by running this build command:
npm run build
What does this build prove?
Vite loads vite.config.ts before compiling the app. A completed build confirms that the React plugin and Tailwind plugin can load together.
You should see the build finish without configuration errors. Your Tailwind plugin is now connected to Vite.
- Return to the editor's file explorer.
- Select src/index.css.
- Replace the stylesheet contents with this Tailwind import:
/* Load Tailwind CSS v4 utilities for the application. */
@import "tailwindcss";
What does this stylesheet do?
The import loads Tailwind's generated styles through the Vite plugin. Classes used by your React components can now change the rendered page.
- Save src/index.css.
- Switch back to the terminal inside my-app.
- Confirm that the stylesheet compiles by running the build again:
npm run build
What should happen now?
The build should finish without CSS errors. This confirms that Vite can resolve the Tailwind import from src/index.css.
The styling pipeline now compiles from end to end. Your components can use Tailwind classes in the next part.
Is the Tailwind build failing?
- Check that tailwindcss appears in package.json.
- Check that @tailwindcss/vite appears in package.json.
- Compare the spelling of tailwindcss() in vite.config.ts.
Help me fix my Tailwind CSS and Vite build error.
Create the baseline Button
A reusable component keeps the design-system classes in one place. Consumers can render the Button without rebuilding its appearance on every page.
- Create a folder named components inside the existing src folder using your editor's file explorer.
- Create Button.tsx inside src/components using the editor's new-file control.
You should now see src/components/Button.tsx in the file explorer. That path gives the reusable component a clear home.
- Add the reusable Button implementation by pasting this code into src/components/Button.tsx:
import type { ButtonHTMLAttributes } from "react"
type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement>
export function Button({ className = "", ...props }: ButtonProps) {
// Keep the design-system appearance in the reusable component.
const baselineClasses =
"rounded-md bg-slate-900 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-slate-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-900"
return (
<button
className={`${baselineClasses} ${className}`}
{...props}
/>
)
}
What does this component do?
- The ButtonProps type accepts the standard properties of an HTML button.
- The baselineClasses value holds the shared design-system appearance.
- The className prop gives future consumers a place to request approved layout classes.
- The remaining props preserve button features such as labels and click handlers.
- Save src/components/Button.tsx.
- Switch back to the terminal inside my-app.
- Confirm that the new component compiles by running:
npm run build
Why build before using the Button?
The TypeScript build checks every source file in the project. A successful result proves that Button.tsx has valid types and syntax.
Your reusable Button now compiles successfully. The page can safely import it.
Does Button.tsx fail to compile?
- Check that the file name uses an uppercase B in Button.tsx.
- Check that every opening brace has a matching closing brace.
- Check that ButtonHTMLAttributes is imported from react.
Help me diagnose the TypeScript error in my Button component.
- Select src/App.tsx in the editor's file explorer.
- Replace the file contents with this baseline page:
import { Button } from "./components/Button"
export default function App() {
// Render the baseline Button without consumer-supplied styling.
return (
<main className="flex min-h-screen items-center justify-center bg-slate-100">
<section className="rounded-xl bg-white p-8 shadow-sm">
<p className="mb-4 text-sm font-medium text-slate-600">
Design system baseline
</p>
<Button>Continue</Button>
</section>
</main>
)
}
What does this page do?
- The import makes the reusable Button available to the application page.
- The page centers a white card against a light background.
- The Button receives only its label. Its colours and spacing come from the reusable component.
✔️ Awesome, I've got everything!
- Save every edited file before starting the development server.
ⓧ I'd like to double check the full code
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
// Compile React and Tailwind CSS through Vite.
plugins: [react(), tailwindcss()],
})
/* Load Tailwind CSS v4 utilities for the application. */
@import "tailwindcss";
import type { ButtonHTMLAttributes } from "react"
type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement>
export function Button({ className = "", ...props }: ButtonProps) {
// Keep the design-system appearance in the reusable component.
const baselineClasses =
"rounded-md bg-slate-900 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-slate-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-900"
return (
<button
className={`${baselineClasses} ${className}`}
{...props}
/>
)
}
import { Button } from "./components/Button"
export default function App() {
// Render the baseline Button without consumer-supplied styling.
return (
<main className="flex min-h-screen items-center justify-center bg-slate-100">
<section className="rounded-xl bg-white p-8 shadow-sm">
<p className="mb-4 text-sm font-medium text-slate-600">
Design system baseline
</p>
<Button>Continue</Button>
</section>
</main>
)
}
- Save src/App.tsx.
- Switch back to the terminal inside my-app.
Before you start the app, what appearance do you expect the reusable Button to have when the page supplies no styling classes?
- Start the development server by running this command:
npm run dev
What does the development server do?
Vite compiles the current source files and prints a local address in the terminal. The process stays active so saved changes can reach the browser.
You should see a local development address in the terminal without build errors.
- Record the local address shown in the terminal here: your local development URL.
- Switch to your browser.
- Enter your local development URL in the browser's address bar.
You should see a pale background with a white card in the centre. The card contains the label Design system baseline above a dark Continue button.
That baseline is now working. You have a reusable Button whose design-system appearance can be compared against every later change.
Does the baseline page look wrong?
- Check that the development server is still running in the terminal.
- Check that src/App.tsx imports ./components/Button with matching capitalization.
- Check that src/index.css contains the Tailwind import.
Help me debug why my baseline Button page does not match the expected design.
Your baseline app is running in the browser. Next up, you will add the linting tools that enforce this Button contract.
Install and Verify the Linter
Your Tailwind CSS baseline Button now shows the design-system appearance that future changes must preserve. The missing piece is an automated check that can inspect agent edits.
ESLint supplies the check runner. @shadcn/lint plugs design-system rules into that runner through a TypeScript parser.
In this step, get ready to:
- Install the lint packages as development dependencies.
- Create a flat configuration that parses the app source.
- Run the lint command against the baseline app.
Install the lint tooling
Development dependencies support checks during development without becoming part of the browser bundle. This installation adds the linter plus the parser it needs for your component files.
- Create a second terminal panel in the code editor from earlier.
- Install the development dependencies in the Tailwind app repository by running:
npm install -D @shadcn/lint eslint @typescript-eslint/parser
What did you install?
- The eslint package runs the lint checks.
- The @shadcn/lint package supplies the design-system rules.
- The @typescript-eslint/parser package reads TypeScript syntax plus TSX markup.
- The -D flag records these packages as development dependencies.
The terminal returns to its prompt when the installation finishes. Your repository now has the tooling needed to inspect component usage.
Did the installation fail?
- Confirm the second terminal is inside the repository containing package.json.
- Check that npm is still available in this terminal.
- Repeat the installation after restoring your network connection.
Help me diagnose this dependency installation.
Create the flat configuration
A flat configuration tells ESLint which files to inspect. It also registers the parser plus the design-system plugin.
- Switch back to the code editor from earlier.
- Use the file-creation control in the Explorer sidebar to create eslint.config.mjs beside package.json.
- Add the baseline lint configuration by pasting this code:
// Load the lint configuration tools
import { plugin as shadcn } from "@shadcn/lint"
import tsParser from "@typescript-eslint/parser"
import { defineConfig } from "eslint/config"
// Parse the app source before lint rules inspect it
export default defineConfig([
{
files: ["**/*.{js,jsx,ts,tsx}"],
languageOptions: {
parser: tsParser,
parserOptions: { ecmaFeatures: { jsx: true } },
},
plugins: { shadcn },
// Keep the rule set empty for the clean baseline
rules: {},
},
])
What does this configuration do?
- The files pattern includes JavaScript plus TypeScript component files.
- The tsParser value lets ESLint parse TypeScript syntax.
- The plugins object loads the Shadcn Lint rules.
- The empty rules object preserves a clean baseline before the Button contract is enabled.
- Save eslint.config.mjs.
✔️ Awesome, I've got everything!
Your saved flat configuration now loads the required plugin plus the TypeScript parser.
ⓧ I'd like to double check the full code
The complete eslint.config.mjs file should match this reference.
// Load the lint configuration tools
import { plugin as shadcn } from "@shadcn/lint"
import tsParser from "@typescript-eslint/parser"
import { defineConfig } from "eslint/config"
// Parse the app source before lint rules inspect it
export default defineConfig([
{
files: ["**/*.{js,jsx,ts,tsx}"],
languageOptions: {
parser: tsParser,
parserOptions: { ecmaFeatures: { jsx: true } },
},
plugins: { shadcn },
// Keep the rule set empty for the clean baseline
rules: {},
},
])
- Add the lint script to package.json by running this command in the second terminal:
npm pkg set scripts.lint="eslint ."
What does this command change?
The command adds lint to the scripts object in package.json. That script gives people plus coding agents one consistent way to run ESLint.
Run the clean lint check
A clean run establishes the state before any Button contract exists. It also proves that ESLint can parse the source files covered by your configuration.
Before you run the check, do you expect the existing baseline Button to produce a lint error?
- Test the baseline repository by running the lint script:
npm run lint
What does a clean pass prove?
- The npm run lint command executes ESLint across the repository.
- The terminal returns to its prompt without reporting lint errors.
- The result becomes your clean baseline for the Button contract.
Lint command not passing?
- Confirm eslint.config.mjs sits beside package.json.
- Check the copied files pattern for missing braces or quotes.
- Confirm the lint script appears inside the scripts object in package.json.
Help me debug this lint setup.
- Switch back to the browser tab from earlier.
You should still see the baseline Button with its original design-system appearance. That clean result gives you a trusted starting line for the rule you add next.
Your linter now parses the app from a repeatable project script. Next, you will turn that foundation into an enforceable Button contract.
Define the Button Style Contract
Your clean ESLint run proved that @shadcn/lint loads inside the Tailwind CSS v4 app. The plugin still has no rule describing which Button styles belong to the design system.
A design-system contract turns that styling decision into a checkable policy. Your contract allows consumers to change the Button's width or vertical margins while the component keeps control of its appearance.
In this step, get ready to:
- Enable the Button contract with the shadcn/no-restyle rule.
- Limit consumer styling to approved width and vertical margin classes.
- Confirm that the unchanged baseline page passes lint.
Define the Button policy
The contract matches the Button by name. Its allowlist becomes the complete set of consumer-supplied classes accepted for that component.
- Return to eslint.config.mjs in your code editor.
- Locate the existing rules object.
- Add this rule entry inside the rules object:
"shadcn/no-restyle": ["error", {
allow: ["layout"],
contracts: [
{ pattern: "^Button$", allow: ["w-full", "mt-*", "mb-*"] },
],
}]
How does the contract work?
- The shadcn/no-restyle rule checks consumer-supplied classes on recognized components.
- The top-level allow: ["layout"] policy establishes the default for recognized components.
- The ^Button$ regular expression matches the Button component name exactly.
- The contract's allow list replaces the default allowlist for Button.
- The w-full pattern permits a consumer to make the Button fill its container.
- The mt-* pattern permits top-margin utilities.
- The mb-* pattern permits bottom-margin utilities.
✔️ Awesome, I've got everything!
Your Button contract now names the only consumer classes that the design system accepts.
ⓧ I'd like to double check the full code
import { plugin as shadcn } from "@shadcn/lint"
import tsParser from "@typescript-eslint/parser"
import { defineConfig } from "eslint/config"
export default defineConfig([
{
files: ["**/*.{js,jsx,ts,tsx}"],
languageOptions: {
parser: tsParser,
parserOptions: { ecmaFeatures: { jsx: true } },
},
plugins: { shadcn },
rules: {
"shadcn/no-restyle": ["error", {
allow: ["layout"],
contracts: [
{ pattern: "^Button$", allow: ["w-full", "mt-*", "mb-*"] },
],
}],
},
},
])
What should you compare?
Check that the rule sits inside the same configuration object as the shadcn plugin. Confirm that the Button contract contains only w-full, mt-*, and mb-*.
- Save eslint.config.mjs.
Verify the clean baseline
The baseline page passes because it uses Button without prohibited consumer styling. A clean result now proves that the new policy loads without rejecting valid usage.
- Take a moment to predict whether the unchanged baseline page satisfies the new Button contract.
- Test the new contract by running this command:
npm run lint
What does this result prove?
The command should return to the terminal prompt without lint errors. This clean pass confirms that shadcn/no-restyle is active while the baseline Button follows its contract.
Lint configuration not loading?
- Check that the new rule is inside the existing rules object.
- Compare each opening bracket in the rule with its matching closing bracket.
- Confirm that shadcn remains registered inside the plugins object.
Help me diagnose why my Button contract is not loading in ESLint.
Confirm the baseline still matches
The contract changes static analysis. Your Button should retain the baseline appearance that was already running in the browser.
- Switch back to the browser tab showing the local app from earlier.
- Refresh the page.
- Inspect the Button's colour.
- Inspect the Button's padding.
- Inspect the Button's shape.
You should see the same baseline Button with its original colour, padding, and shape. Great work. The visual design now stays unchanged while ESLint enforces how consumers may position the component.
Your Button contract is active. Next, you will give the linter an off-system Button change to catch.
Catch an Off-System Restyle
The clean ESLint pass from the last step proves that your current Tailwind CSS page follows the Button contract.
A clean pass only matters if @shadcn/lint can reject a real violation. This step gives the Button an off-system colour and padding.
In this step, get ready to:
- Apply prohibited colour and padding classes to the existing Button.
- Compare the restyled Button with its baseline appearance.
- Confirm the design contract rejects the off-system restyle.
Restyle the Button off-system
The Button owns its baseline styles inside src/components/Button.tsx. Its className prop in src/App.tsx is the consumer entry point for extra classes.
- Switch back to src/App.tsx in the code editor from earlier.
- Find the existing Button element on the application page.
- Set its className prop to use w-full, bg-red-500, and px-8.
- Save src/App.tsx.
Your running page refreshes after the save.
- Return to the browser tab from earlier to compare the changed Button with the baseline view.
You should see the Button stretch across its available width. Its red colour and extra horizontal padding create the deliberate design drift.
Button still looks unchanged?
- Confirm that src/App.tsx is saved.
- Check that the existing Button receives the className prop.
- Check that the prop contains bg-red-500 and px-8.
Help me find why the consumer classes on my Button are not visible.
Run lint against the restyle
The design contract now faces a real test. The linter must distinguish the permitted layout class from the prohibited colour and padding classes.
- Add a second terminal session from your code editor's terminal panel.
- Predict which of the three consumer classes the Button contract rejects.
- Test the deliberate restyle by running:
npm run lint
What did lint catch?
- The shadcn/no-restyle rule compares the consumer classes with the Button contract.
- The rule accepts w-full because the contract permits it.
- The rule identifies bg-red-500 and px-8 as prohibited consumer classes.
- The report provides guidance for removing the off-system styling from the call site.
That failing lint run is the proof you wanted. Your design-system contract now catches a visible off-system change before it can slip through review.
Lint still passes?
- Confirm that src/App.tsx is saved with bg-red-500 in the className prop.
- Confirm that the same className prop includes px-8.
- Check that eslint.config.mjs still applies shadcn/no-restyle to the app's TypeScript and TSX files.
Help me debug why shadcn/no-restyle passes despite prohibited Button classes.
Your contract has caught the design drift. Next, you will give that report to your coding agent and verify that its fix restores a clean lint pass.
Fix the Button with an Agent
Your @shadcn/lint rule has already exposed the off-system classes on the Button. That warning protects the design system when the next edit follows its guidance.
In this step, a coding agent will remove the prohibited Tailwind CSS colour classes from src/App.tsx. The agent will also remove the prohibited padding classes.
You will use ESLint to verify the repaired contract. The browser then confirms that the Button has returned to its baseline appearance.
In this step, get ready to:
- Use the lint guidance to focus a coding agent on the Button usage.
- Prove the updated design-system contract with a clean lint run.
- Confirm the Button's baseline appearance in the browser.
Guide the agent with the lint message
The lint message gives the agent a concrete repair target. A narrow prompt keeps the edit inside the consumer code that caused the violation.
- Switch back to the coding agent connected to your app repository.
- Submit this focused repair request to the agent:
Fix the current shadcn/no-restyle lint error.
Use the linter's guidance to update src/App.tsx. Remove every prohibited colour or padding Tailwind class from the Button usage.
Keep only consumer classes allowed by the existing contract: w-full, mt-*, and mb-*.
Do not change src/components/Button.tsx or eslint.config.mjs.
Show me the diff before finalizing.
Why is this prompt focused?
- The request scopes the repair to src/App.tsx.
- The allowlist preserves legitimate width or margin utilities.
- The file guard protects the contract in eslint.config.mjs.
- The file guard also protects the baseline styles in src/components/Button.tsx.
- Review the proposed changes in the agent's diff.
- Confirm that only src/App.tsx changes.
- Reject any proposal that changes src/components/Button.tsx or eslint.config.mjs.
- Approve the src/App.tsx edit using your agent's edit confirmation control.
The accepted edit should remove the colour utilities from the Button usage. It should also remove the padding utilities.
Any remaining consumer classes should match w-full, mt-*, or mb-*.
Agent changed the wrong files?
- Reject the proposed edit if it changes the component or lint configuration.
- Send the current lint diagnostic to the agent in a new message.
Ask for help reviewing the proposal: Help me check whether my coding agent's diff fixes the no-restyle violation without changing the Button component or lint contract.
Prove the contract with lint
Source changes can look reasonable while still violating the design-system contract. A clean lint run provides machine-checkable proof that the repair satisfies the configured rule.
- Create a new session in the terminal panel from earlier.
Before you run the check, do you expect the same design-system violation or a clean result?
- Test the agent's repair by running this command:
npm run lint
What does this command prove?
- The npm command runs the repository's existing lint script.
- ESLint loads eslint.config.mjs for the project rules.
- A clean result proves that the Button usage satisfies shadcn/no-restyle.
You should see the lint command finish without a shadcn/no-restyle error.
That is the hard part done. Your agent has brought the Button usage back inside the shared contract.
Lint still reports a violation?
- Read the remaining diagnostic to identify each prohibited consumer class.
- Return the remaining diagnostic to your coding agent.
- Check that the terminal session points to the folder containing package.json.
Ask for help with the remaining diagnostic: Help me interpret this remaining no-restyle lint error and prepare a focused repair for src/App.tsx.
Compare the Button with the baseline
Lint proves that the consumer classes follow the contract. The browser confirms that the visible Button has returned to the design system.
Before you refresh, do you expect the Button's visual styling or its allowed layout to match the baseline?
- Switch back to the local app page that remains open in your browser.
- Refresh the page using your browser's reload control.
- Compare the Button with the baseline appearance from earlier.
What should you see?
- The Button uses the baseline colours defined in src/components/Button.tsx.
- The Button uses the baseline padding defined in src/components/Button.tsx.
- Any retained w-full class may still affect its width.
- Any retained mt-* or mb-* class may still affect its spacing.
You have closed the feedback loop. The linter identified the drift before the agent restored the shared Button design.
Secret mission
Run a Design Contract Regression Drill
Temporarily reintroduce an off-system Button restyle. Prove your lint guardrail catches the regression. Use its guidance to return the Button to its baseline design.
Clean Up Your Resources
Clean Up Your Resources
Choose whether to keep your local project active, pause its development server, or remove the repository entirely. This project runs on your machine, so there are no ongoing cloud charges.
Resources you used:
- Your local Tailwind CSS v4 application repository. It contains the app code, the ESLint configuration, @shadcn/lint, plus the TypeScript parser dependency.
- The development server process that serves the local app in your browser.
Keep everything running
No action is needed. This option keeps the lint-enforced app available while you continue experimenting with the Button contract.
- The development server continues serving the app at its local URL.
- The repository keeps your app code, lint configuration, plus installed project dependencies.
- Your resources remain local with no ongoing cloud charges.
Pause - I'll come back to this later
Stop the development server to free its local port while preserving the complete repository. Your design-system contract remains ready for the next coding session.
- Return to the terminal panel from earlier.
- Press Ctrl+C in the terminal.
You should see the terminal prompt return. Refreshing the local URL should show that the development server is no longer available.
Delete - I don't want to use this again
Deleting the repository permanently removes the app code, lint contract, plus project dependencies from your machine. Your separately installed tools remain available for other projects.
- Back up any project work you want to keep outside the repository.
- Return to the terminal panel from earlier.
- Press Ctrl+C to stop the development server.
You should see the terminal prompt return. This confirms that the local server process has stopped.
- Switch back to the code editor from earlier.
- Reveal the Tailwind v4 app repository in Finder on macOS or File Explorer on Windows from the editor's file sidebar.
You should see the repository folder selected in your file manager. This is the folder that contains the complete local project.
- Move the repository folder to Trash on macOS or Recycle Bin on Windows.
- Empty Trash on macOS or Recycle Bin on Windows to complete the deletion.
The repository should no longer appear in your file manager. Its app code, lint configuration, plus installed project dependencies are now removed.
Nice Work!
Nice Work!
You made it. You've built a Tailwind CSS v4 app where ESLint and @shadcn/lint protect the Button component from off-system restyling.
What you learned:
- Defined a design-system contract for Button that permits only approved layout classes.
- Triggered a lint failure with prohibited colour and padding classes. Read the shadcn/no-restyle message to identify the contract breach.
- Guided a coding agent with the linter's feedback. Confirmed a clean lint pass after the Button returned to its baseline appearance.
- Secret Mission: Extended the project through an optional challenge that applied the same contract-first thinking.
Ready to quiz yourself?