How to Set Up ESLint and Prettier Together in 2026 (No Conflicts)
ESLint handles code quality. Prettier handles formatting. They should never fight — but without the right glue, they absolutely will. Here's the modern setup that makes them cooperate, cleanly and permanently.
Why ESLint and Prettier conflict in the first place
ESLint is a linter — it analyzes your code for bugs, bad patterns, and violations of rules you've configured. Prettier is a formatter — it takes your code and reprints it with consistent style: indentation, quotes, trailing commas, line length.
The problem is that ESLint also ships formatting rules. Things like quotes, semi, indent, and max-len. When Prettier reformats a file and ESLint then checks it, those two opinions on what the code should look like can clash — leaving you with an auto-fix loop or red squiggles on perfectly valid code.
The solution isn't to pick one over the other. It's to let each tool own its lane: Prettier owns all formatting decisions, and ESLint is told to stay out of that lane entirely. That's exactly what this guide sets up.
Division of responsibility
ESLint owns →
- → Unused variables
- → Unreachable code
- → Missing dependencies in hooks
- → Accessibility violations
- → Import order
- → Security anti-patterns
Prettier owns →
- → Indentation (tabs vs spaces)
- → Quote style (single vs double)
- → Trailing commas
- → Semicolons
- → Line length / wrapping
- → Bracket spacing
.webp)
What changed: ESLint flat config is now the default
If you haven't touched an ESLint config recently, you'll notice something: the old .eslintrc.json / .eslintrc.js format is deprecated. ESLint v9 — stable since mid-2024 and now universally adopted — uses a single eslint.config.js (or eslint.config.mjs) file in flat config format.
Flat config is simpler, explicit, and composable. Instead of a cascade of nested config files that merge in unpredictable ways, you export an array of config objects. Each object applies rules to a specific set of files. What you see is what you get.
Old format (deprecated)
// .eslintrc.json
{
"extends": ["eslint:recommended"],
"rules": {
"semi": ["error", "always"]
}
}New flat config (current)
// eslint.config.js
import js from "@eslint/js";
export default [
js.configs.recommended,
{
rules: { "no-unused-vars": "warn" }
}
];All examples in this guide use flat config. If you're migrating from the old format, ESLint ships a migration tool: npx @eslint/migrate-config .eslintrc.json.
Step 1 — Install the packages
You need four packages to get a clean, conflict-free setup:
npm install --save-dev \
eslint \
prettier \
eslint-config-prettier \
@eslint/jseslintThe linter itself. Finds code quality issues and enforces rules.prettierThe formatter. Reprints your code with consistent style.eslint-config-prettierThe glue. Disables every ESLint rule that would conflict with Prettier's output.@eslint/jsThe official ESLint JavaScript config package needed for flat config.Why not eslint-plugin-prettier?
You might have seen eslint-plugin-prettier in older guides. This plugin runs Prettier as an ESLint rule and reports formatting differences as lint errors. It works, but the Prettier team no longer recommends it — it makes editor feedback slower and blurs the line between the two tools. The approach in this guide (using eslint-config-prettier only) is cleaner.
Step 2 — Configure Prettier
Prettier works with zero configuration — it has sensible defaults for everything. But most teams have opinions on at least a few settings. Create a .prettierrc (JSON) or prettier.config.js in your project root:
// prettier.config.js
/** @type {import("prettier").Config} */
const config = {
// Most common preferences — adjust to taste
semi: true, // Add semicolons
singleQuote: true, // Use single quotes in JS/TS
jsxSingleQuote: false, // Use double quotes in JSX (matches HTML convention)
trailingComma: "es5", // Trailing commas where valid in ES5
printWidth: 100, // Wrap lines at 100 chars
tabWidth: 2, // 2-space indentation
useTabs: false, // Spaces, not tabs
bracketSpacing: true, // { foo: bar } not {foo: bar}
bracketSameLine: false, // JSX closing > on its own line
arrowParens: "always", // Always include parens: (x) => x
endOfLine: "lf", // Unix line endings — keeps git diffs clean on Windows
};
export default config;Also create a .prettierignore to tell Prettier what not to touch:
# .prettierignore
node_modules
.next
dist
build
coverage
*.min.js
*.generated.*
publicStep 3 — Configure ESLint (flat config)
Now create eslint.config.js in your project root. The key move here is adding eslint-config-prettier last in the array — it needs to override any formatting rules that come before it:
// eslint.config.js
import js from "@eslint/js";
import prettierConfig from "eslint-config-prettier";
export default [
// 1. ESLint's built-in recommended rules (no formatting rules)
js.configs.recommended,
// 2. Your custom rules
{
files: ["**/*.{js,jsx,mjs,cjs}"],
rules: {
"no-console": "warn",
"no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
"prefer-const": "error",
"no-var": "error",
eqeqeq: ["error", "always"],
},
},
// 3. Prettier LAST — disables all ESLint formatting rules
// that could conflict with Prettier's output
prettierConfig,
];Order matters
eslint-config-prettier must be the last item in the array. It works by turning rules off — if anything comes after it and re-enables a formatting rule, you're back to conflicts.
Step 4 — TypeScript support
If your project uses TypeScript, install the TypeScript ESLint toolchain:
npm install --save-dev \
typescript-eslint \
@typescript-eslint/parser \
@typescript-eslint/eslint-pluginThen update your eslint.config.js:
// eslint.config.js
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import prettierConfig from "eslint-config-prettier";
export default tseslint.config(
// JS recommended
js.configs.recommended,
// TypeScript recommended (type-checked)
...tseslint.configs.recommendedTypeChecked,
// TypeScript parser options — point to your tsconfig
{
languageOptions: {
parserOptions: {
project: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
// Your custom rules
{
files: ["**/*.{ts,tsx}"],
rules: {
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": [
"warn",
{ argsIgnorePattern: "^_" },
],
"@typescript-eslint/consistent-type-imports": "error",
},
},
// Non-TypeScript files (config files, scripts)
{
files: ["**/*.{js,mjs,cjs}"],
...tseslint.configs.disableTypeChecked,
},
// Prettier always last
prettierConfig,
);recommendedTypeChecked vs recommended
The TypeChecked variant enables rules that require type information from the TypeScript compiler — they catch more real bugs but are slower to run. For large repos, you can switch to tseslint.configs.recommended for faster CI at the cost of a few rules.
Step 5 — React / Next.js support
For React projects (including Next.js), add the React-specific ESLint plugins:
npm install --save-dev \
eslint-plugin-react \
eslint-plugin-react-hooks \
eslint-plugin-jsx-a11y// eslint.config.js (React / Next.js additions)
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import reactPlugin from "eslint-plugin-react";
import reactHooksPlugin from "eslint-plugin-react-hooks";
import jsxA11y from "eslint-plugin-jsx-a11y";
import prettierConfig from "eslint-config-prettier";
export default tseslint.config(
js.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
// React
{
files: ["**/*.{jsx,tsx}"],
plugins: {
react: reactPlugin,
"react-hooks": reactHooksPlugin,
"jsx-a11y": jsxA11y,
},
settings: {
react: { version: "detect" },
},
rules: {
...reactPlugin.configs.recommended.rules,
...reactHooksPlugin.configs.recommended.rules,
...jsxA11y.configs.recommended.rules,
// Not needed with React 17+ new JSX transform
"react/react-in-jsx-scope": "off",
"react/prop-types": "off", // TypeScript handles this
},
},
// Prettier always last
prettierConfig,
);If you're on Next.js, you can also extend the official Next.js ESLint config. Run npx next lint --init and it will scaffold the config for you — then append prettierConfig at the end.
Step 6 — VS Code editor integration
The config files alone don't give you in-editor feedback. You need the right VS Code extensions and workspace settings. Install:
- → ESLint —
dbaeumer.vscode-eslint - → Prettier – Code formatter —
esbenp.prettier-vscode
Then add a .vscode/settings.json to your repo so everyone on the team gets the same behaviour automatically:
// .vscode/settings.json
{
// Prettier formats on save
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
// ESLint auto-fixes on save (import order, etc.)
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
// Make sure ESLint uses the flat config
"eslint.useFlatConfig": true,
// Validate these file types with ESLint
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
]
}Commit this file
Committing .vscode/settings.json means every developer who clones the repo gets format-on-save working immediately — no manual setup, no "it worked on my machine" formatting diffs.
Step 7 — Pre-commit hooks with lint-staged
Editor integration is great for developers actively working in a file. But it doesn't catch someone who edits a file in a different editor, or runs a script that generates code. Pre-commit hooks are your safety net — they run ESLint and Prettier on staged files right before every commit.
npm install --save-dev husky lint-staged
# Initialize husky
npx husky initAdd the lint-staged config to your package.json:
// package.json
{
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md,css,scss,yaml,yml}": [
"prettier --write"
]
}
}Then configure the pre-commit hook:
# .husky/pre-commit
npx lint-stagedNow whenever a developer runs git commit, lint-staged grabs only the files they've staged, runs ESLint (with auto-fix) and Prettier on them, and aborts the commit if there are unfixable errors. Clean code becomes the path of least resistance.
NPM scripts to add to package.json
A consistent set of scripts makes it easy for anyone on the team (and your CI pipeline) to lint and format the whole project:
// package.json
{
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
"check": "npm run lint && npm run format:check"
}
}| Script | What it does |
|---|---|
| npm run lint | Reports all ESLint violations across the project |
| npm run lint:fix | Auto-fixes everything ESLint can fix automatically |
| npm run format | Rewrites all files through Prettier |
| npm run format:check | Checks formatting without rewriting — use in CI |
| npm run check | Runs both lint and format:check — your CI gatekeeper |
Troubleshooting common issues
Even with a clean setup, you'll occasionally hit an edge case. Here are the most common problems and how to resolve them:
?ESLint and Prettier are still fighting over quotes / semicolons
Make sure eslint-config-prettier is the last item in your config array. Run `npx eslint-config-prettier src/index.ts` to see which rules are being disabled — if your formatting rules aren't in the output, they're not being picked up by prettier's config.
?ESLint says 'Could not find config file'
You likely have a mix of old (.eslintrc.*) and new (eslint.config.js) config files. ESLint v9 prefers the flat config; delete any .eslintrc.* files or set ESLINT_USE_FLAT_CONFIG=false if you need the old behavior temporarily.
?TypeScript rules run slowly in large repos
Switch from recommendedTypeChecked to recommended in your tseslint config. You lose type-aware rules but gain much faster lint times. Alternatively, scope type-checked rules to src/** and exclude node_modules, test files, and generated code.
?Prettier reformats files that ESLint then flags as errors
This is the exact scenario eslint-config-prettier prevents. If you're seeing it, a plugin you've added is re-enabling formatting rules after prettier's config. Check plugin order — no formatting-related plugin should come after prettierConfig.
?VS Code isn't formatting on save
Check that the Prettier extension is set as the default formatter: open a JS/TS file, press Cmd/Ctrl+Shift+P, run 'Format Document With...', and select Prettier. If it's not listed, the extension may not be installed or the workspace settings aren't being picked up.
You're done — here's what you now have
That's a complete, conflict-free ESLint and Prettier setup for 2026. Let's recap what's in place:
- ✓ ESLint v9 flat config catching real code quality issues
- ✓ Prettier handling all formatting decisions with zero interference from ESLint
- ✓ TypeScript and React plugins configured correctly
- ✓ VS Code formatting on save for every developer on the team
- ✓ Pre-commit hooks that enforce standards before code ever reaches the repo
- ✓ npm scripts for running checks in CI
The result is a codebase where formatting is never a PR comment again, and ESLint's energy is spent on things that actually matter — catching bugs and enforcing patterns, not arguing about semicolons.