Development

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
How to Set Up ESLint and Prettier Together in 2026 (No Conflicts)

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/js
eslintThe 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.*
public

Step 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-plugin

Then 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:

  • ESLintdbaeumer.vscode-eslint
  • Prettier – Code formatteresbenp.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 init

Add 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-staged

Now 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"
  }
}
ScriptWhat it does
npm run lintReports all ESLint violations across the project
npm run lint:fixAuto-fixes everything ESLint can fix automatically
npm run formatRewrites all files through Prettier
npm run format:checkChecks formatting without rewriting — use in CI
npm run checkRuns 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.

Feedback

Live