How to Set Up a React App with Vite, ESLint, Tailwind, and Prettier (The Easy Way)


If you’ve ever tried to get your React app’s tooling just right, you know it can feel like wrestling a hydra — add one thing, and two more errors pop up. You want Vite for blazing-fast dev, ESLint to keep your code clean, Prettier to keep it pretty, and Tailwind to style everything without losing your mind.
But what if you want to catch lint errors as you save, not just when you build? Or avoid that hair-pulling moment when you pull someone else’s code and bam — a flood of linting errors screams at you?
That’s exactly what we’ll cover here — a simple setup that gets all four tools playing nice, so you can catch problems early and keep your team’s code clean without the endless back-and-forth..
Grab a coffee (or scream into a pillow if you’ve been on this for too long), and let’s dive in.
Step 1: Create a new React + Vite project
npm create vite@latest
You will be prompted to fill out some configuration details for your project.
For clarity, I’m running two versions side-by-side — one with TypeScript and one with JavaScript — so you can see how both setups look.
These are the details you will be prompted to enter:
- Project name:
Framework
Framework variant:
I chose Javascript + SWC here. SWC (Speed Web Compiler) is just a super-fast alternative to Babel for compiling your code — think of it as a faster way to transform your modern JavaScript and TypeScript into browser-ready code without any extra hassle.
Afterwards, you should see the project scaffolding created for you. All that’s left is to install the dependencies and run the dev script: npm run dev
, to view your app locally.
Step 2: Styling with TailwindCSS💅
Tailwind is a utility-first CSS framework that lets you build custom designs quickly without writing a lot of custom CSS. It’s super popular for its flexibility and ease of use. Let’s get it set up!
In your terminal:
npm install tailwindcss @tailwindcss/vite
Next, you will add tailwind to your Vite configuration as a plugin.
// vite.config.js -- this will be vite.config.ts if Typescript was selected
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
})
Then, import tailwind styles into your main css file. This can be index.css
or App.css
/* index.css */
@import "tailwindcss";
:root {
/* ... */
}
Once that’s done, you can use Tailwind utility classes anywhere in your app. For example, adding the class text-red-600
to the <h1>
element will color the text a vibrant red.
// src/App.jsx (or src/App.tsx)
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'
function App() {
const [count, setCount] = useState(0)
return (
<>
<div>
<a href="https://vite.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1 className='text-red-600'>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</>
)
}
export default App
That’s it! We’ve set up styling.
Step 3: Code Formatting & Linting with ESLint and Prettier
Keeping your code clean and consistent is key — and that’s where ESLint and Prettier come in. ESLint catches potential errors and enforces coding standards, while Prettier formats your code so it always looks neat. Tabs vs. spaces debates? Those are for the kickoff meeting — agree once, enforce forever, and save everyone the headache.
But honestly, if you’re still using spaces… hey, i love that for you. Bold choice. 😄
Sidebar: But I’m also saying, valid reason to break up with someone️⬇️🤷♀️
So, how do we set this up? We utilize the prettier library.
First, we install it.
npm install --save-dev prettier
Then, we add a .prettierrc
file in the root of our project to set it up.
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 120,
"useTabs": true,
"endOfLine": "auto"
}
We also set up a format script in package.json
. This command formats all ts/tsx/js/jsx/json/css
files in the src folder when run in the terminal.
// package.json
"scripts": {
// ...
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css}\""
}
To make sure Prettier is actually used when you save files, you should enable format-on-save in your editor (like VS Code)
In VS Code, set:
"editor.formatOnSave": true
You could also set Prettier as your default formatter so when you press Shift + Alt + F, your code gets formatted
And when we build:
Now, while Prettier handles formatting (tabs, spacing, quotes, etc.), ESLint handles code quality and potential bugs (like unused variables or bad patterns). But here’s the cool part:
You can make ESLint enforce Prettier rules by using a plugin — so if someone dares commit misaligned code or rogue spaces, ESLint will scream. (Which is always better than you doing it😉)
Step 3.2: ESLint
When we created our React app with Vite, ESLint was automatically included in the project setup — so there’s no need to install it manually. If you selected SWC during setup, you should see an eslint.config.js
file in the root of your project. If you're using Babel instead, you might see a different ESLint config format like .eslintrc.js
or .eslintrc.json
. They’re essentially the same thing — just in different formats. eslint.config.js
follows the newer, more modern flat config format, while .eslintrc.*
uses the traditional style.
// eslint.config.js
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)
Here's a quick breakdown of the key properties of your ESLint configuration:
ignores: This references files or directories ESLint should skip. Here,
dist
is ignored because it's the build output, and linting there is unnecessary.extends: This sets up base rule sets to inherit. We are using the recommended rules from the core ESLint (
js.configs.recommended
) and TypeScript ESLint (tseslint.configs.recommended
) plugins for strong baseline linting.files: targets only
.ts
and.tsx
files, so ESLint focuses on your TypeScript React code. This will bejs
andjsx
for Javascript template users.languageOptions: Configures language features like ECMAScript 2020 syntax support and sets global variables (browser globals here).
plugins: adds ESLint plugins to support React hooks and React refresh features during development.
rules: Customizes specific linting rules, such as those recommended for React Hooks and React Refresh to catch common issues and enforce best practices.
To enforce formatting rules and prevent conflicts between ESLint and Prettier, you can integrate Prettier directly into ESLint’s workflow.
This involves, first, adding the eslint-plugin-prettier
plugin and enabling its recommended config, which runs Prettier as an ESLint rule — meaning any formatting issues show up as lint errors or warnings.
// eslint.config.js
// other imports
import prettier from 'eslint-plugin-prettier';
export default tseslint.config(
{ ignores: ['dist'] },
{
// ...,
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
prettier,
},
// ...
},
)
We haven’t enforced this yet on ESLint. To do that, it has to be added as a rule. (You can skip to the end to learn how we enforce prettier formatting rules).
ESLint rules are a set of regulations used to validate if your code meets a certain coding standard.
For instance, a rule we can add is one to ensure all imports and variables are actually used in the code.
Unused imports and variables not only clutter your code and reduce readability, but they can also increase your bundle size unnecessarily. This leads to larger files, slower load times, and degraded app performance. The “little things” count.
// eslint.config.js
// other imports
import prettier from 'eslint-plugin-prettier';
export default tseslint.config(
{ ignores: ['dist'] },
{
// ...
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
'no-unused-vars': ['error', { vars: 'all', args: 'after-used', ignoreRestSiblings: false }],
},
},
)
But we already have this setup on typescript-eslint
which we get out the box when we run npm create vite
. So, you can delete that or you will have duplicate lint errors in your terminal.
To test, import an unused useEffect
in App.tsx
and run the lint script:
You can add more rules to the rules object. Another rule example is no-console
, which will warn or error on console.log
statements to avoid leaving debugging logs in production code.
Here’s how we can test that:
rules: {
// other rules...
'no-console': ['warn', { allow: ['warn', 'error'] }],
}
This setup will warn whenever you use console.log
, console.info
, etc., but will allow console.warn
and console.error
, which are often useful for logging warnings and errors in development or production.
Next, add a random log to App.tsx
:
function App() {
const [count, setCount] = useState(0)
console.log('This is a random unnecessary log to piss the lead off.')
return (
// ...
)
}
And when you run npm run lint
:
For a more exhaustive list of rules, click here.
You can also add linting to your build command to catch these issues or potential bugs, so your job fails before the build even starts — saving you from wasting time compiling broken code.
Note: We changed the ESLint rule from "warn"
to "error"
so the process halts on violations. If it were still set to "warn"
, the build would continue and the issue would only be logged to the console.
// lint + build command
{
"scripts": {
// ...,
"build": "eslint . && tsc -b && vite build"
}
}
Or, in your CI pipeline, you can separate linting into its own job that runs before your build and deploy jobs — so your flow stops early when linting issues are found, not after wasting resources.
Now, are you a Lead whose build job regularly fails from linting issues (after what feels like years of running it) or your CI workflow bombs at the lint step just because someone forgot to run npm run lint
before opening their PR?
Maybe you’ve been this close to rage-quitting… or committing assault and battery on having words with that one dev who swears “linting is optional.” Maybe you just like your codebase consistent, strict, and guarded by angry style rules. Cough cough, or maybe you just have a few control issues.
An easy way to do this is to catch lint errors early with the Vite plugin. By adding vite-plugin-eslint
, linting happens on every save and compile, showing errors and warnings right in your terminal or console as you work—no need to wait for builds or CI checks.
To add it, simply install the plugin:
npm install vite-plugin-eslint --save-dev
Add it to vite.config.ts
// vite.config.ts
// other imports
import eslint from 'vite-plugin-eslint'
export default {
plugins: [
react(),
tailwindcss(),
eslint({
emitWarning: true, // emits to the console if there is a warning
emitError: true, // emits to the console if there is an error
failOnError: false, // hides the stack trace
}),
],
}
If you’re using typescript, you would most likely encounter this error: Cannot find module 'vite-plugin-eslint' or its corresponding type declarations
. I tried getting a declaration file for it and gave up approximately 0.05 seconds later. The quick fix? Simply rename your config file from .ts
to .js
and voilà — it works like a charm.
If you happen to find a proper, officially approved way to handle this, please share it in the comments below — I’d love to see it!
Now that we’ve done that, we restart our server and voila, we have Vite checking for linting issues on every save:
Other ways we could find linting issues before our workflow run is:
Pre-commit hooks with Husky:
Tools like Husky let you run linting automatically before every commit, so if there are errors, the commit is blocked until they’re fixed.--> install husky npm install husky --save-dev --> initialize npx husky install --> Add a pre-commit hook that runs lint npx husky add .husky/pre-commit "npm run lint"
This makes sure nobody can commit broken or messy code, keeping your codebase clean and everyone happy.
CI/CD pipelines that block merges on lint errors: Even with local safeguards, sometimes errors sneak through. So, just like we discussed earlier, integrating linting into your CI/CD pipeline is critical - ideally early on in the flow before resource-intensive steps like infrastructure provisioning and deployment. Set up your pipeline to run
npm run lint
as a required step, so if lint errors exist, the build and deploy jobs will be terminated.IDE integrations for live linting feedback: Finally, encourage your team to install ESLint and Prettier extensions in their IDEs. This gives instant feedback on linting and formatting issues while typing — catching problems even before they hit save.
Like I mentioned earlier, you can enforce your prettier rules using ESLint. For instance, if you want only single quotes on your source code and want to enforce semi-colons, you define your prettier configuration like so:
{
// ...
"semi": true,
"singleQuote": true,
// ...
}
And add prettier to the eslint configuration file:
// eslint.config.js
// other imports
import prettier from 'eslint-plugin-prettier';
export default tseslint.config(
{ ignores: ['dist'] },
{
// ...
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
prettier,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
'no-console': ['error', { allow: ['warn', 'error'] }],
'prettier/prettier': 'error', // rule to trigger an error if prettier rules do not pass
},
}
);
Remember to install the new library:
npm i --save-dev eslint-plugin-prettier
Now if double quotes are used:
Ahhh!! Isn’t that absolutely beautiful? 😍 A glorious red sea of 'fix your sh*t' — brought to you by Prettier & ESLint your friendly formatting tyrant.
Note: I set failOnError to true to be able to view the errors on my browser instead of only on my terminal.
// vite.config.ts
// other imports
import eslint from 'vite-plugin-eslint'
export default {
plugins: [
react(),
tailwindcss(),
eslint({
// ...other same as before
failOnError: true, // shows the stack trace
}),
],
}
Conclusion
This started out as a simple “How to set up Vite + React + Tailwind + ESLint + Prettier” guide… and somehow turned into a drawn out monologue about linting hygiene and CI-induced trauma 😅 But hey — we did get that project properly set up with all the essentials.
Now go forth and npm run lint
like your merge depends on it. Happy Coding✨
Subscribe to my newsletter
Read articles from Tito Adeoye directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
