Skip to main content

ESLint 10 Flat Config Migration Guide 2026

·PkgPulse Team
0

ESLint 10.0.0 shipped in February 2026 with a single breaking change that affects every JavaScript project using a .eslintrc.* config: legacy config is gone. No more .eslintrc.js, .eslintrc.json, or .eslintrc.yml. The new standard is eslint.config.js — a flat config file that's been optional since ESLint 8 and mandatory since ESLint 10.

This guide covers the practical migration path. If you're running ESLint 9 and want to upgrade, this is the step-by-step process.

TL;DR

What changed: .eslintrc.* files are no longer supported. ESLint 10 only reads eslint.config.js (or .mjs/.cjs). Who is affected: Every project on ESLint < 9 that hasn't migrated. Migration time: 30–60 minutes for most projects; longer for complex plugin setups. ESLint 9 bridge: If you need to defer migration, ESLINT_USE_FLAT_CONFIG=false still works in ESLint 9 but is removed in ESLint 10.

Key Takeaways

  • ESLint 10 removes the ESLINT_USE_FLAT_CONFIG=false escape hatch — you must migrate or stay on ESLint 9
  • eslint.config.js is plain JavaScript — no JSON schema constraints, full require/import support
  • Global ignores replace .eslintignore — the ignores array in config handles what .eslintignore did
  • typescript-eslint v8+ is required for ESLint 10 compatibility; v7 doesn't support flat config
  • eslint-config-next has an open ESLint 10 compatibility issue (April 2026) — use --legacy-peer-deps as a workaround
  • eslint:recommended syntax changed — use js.configs.recommended from @eslint/js

At a Glance: Legacy vs Flat Config

FeatureLegacy .eslintrcFlat Config eslint.config.js
File formatJSON / YAML / JSJavaScript only
Config loadingCascading (directory hierarchy)Single flat array
Plugin loadingplugins: ["react"] (string)import reactPlugin from 'eslint-plugin-react'
Extendsextends: ["airbnb"]Spread plugin configs directly
Ignore file.eslintignoreignores: [...] in config
env keyenv: { browser: true }globals from globals package
TypeScript config@typescript-eslint/parser in parser fieldtseslint.config(...) wrapper
ESLint version1–98–10 (required in 10)

Why ESLint 10 Removed Legacy Config

The flat config system was introduced in ESLint 8 as opt-in. The design goals were:

  1. Eliminate cascading confusion — legacy config used a directory hierarchy where child .eslintrc files overrode parent configs. This made it hard to understand which rules applied to which files, especially in monorepos.
  2. Make config explicit — flat config is a single file with an array of config objects. There's no hidden merging, no root: true hack, and no surprising overrides.
  3. Enable native ES modules — legacy config required CommonJS patterns in many cases. eslint.config.mjs supports top-level await and native ESM imports.
  4. Plugin naming clarity — strings like "plugin:react/recommended" were opaque. Flat config requires importing plugins explicitly, making the dependency visible.

ESLint 9 gave teams two years to migrate with the ESLINT_USE_FLAT_CONFIG=false escape hatch. That escape hatch is removed in ESLint 10.

Step 1: Check Your Current ESLint Version and Config

npx eslint --version  # check current version
ls .eslintrc*         # find legacy config files

ESLint 10 also raised the Node.js floor: ^20.19.0 || ^22.13.0 || >=24 is required. Node 20.18, all of v21, and all of v23 are no longer supported.

If you're on ESLint 8 or earlier, upgrade to ESLint 9 first to validate your migration before jumping to 10:

npm install eslint@9 --save-dev

Verify your project still lints correctly, then upgrade to 10:

npm install eslint@10 --save-dev

Step 2: Run the ESLint Migration Helper

ESLint ships a migration utility that converts your legacy config automatically:

npx @eslint/migrate-config .eslintrc.js

This generates a eslint.config.mjs as a starting point. It handles:

  • Converting rule definitions
  • Mapping extends arrays to plugin config spreads
  • Translating env settings to globals entries
  • Moving ignores patterns

The output requires manual review — the utility handles the mechanical conversion but can't resolve all edge cases. Treat it as a starting draft.

Step 3: Replace extends with Explicit Imports

Legacy config relied on string-based extends that loaded plugins by convention. Flat config requires explicit imports.

Legacy:

// .eslintrc.js
module.exports = {
  extends: [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:react/recommended",
    "next/core-web-vitals"
  ]
}

Flat config:

// eslint.config.mjs
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import reactPlugin from "eslint-plugin-react";
import nextPlugin from "@next/eslint-plugin-next";

export default tseslint.config(
  js.configs.recommended,
  ...tseslint.configs.recommended,
  {
    plugins: {
      react: reactPlugin,
      "@next/next": nextPlugin,
    },
    rules: {
      ...reactPlugin.configs.recommended.rules,
      ...nextPlugin.configs["core-web-vitals"].rules,
    },
  }
);

The key change: every plugin must be import-ed directly. The string-based registry that extends used is gone.

Step 4: Migrate env to globals

Legacy config used env keys to declare global variables for specific environments. Flat config uses the globals package directly.

npm install globals --save-dev

Legacy:

env: {
  browser: true,
  node: true,
  es2022: true
}

Flat config:

import globals from "globals";

export default [
  {
    languageOptions: {
      globals: {
        ...globals.browser,
        ...globals.node,
        ...globals.es2022,
      }
    }
  }
];

Step 5: Replace .eslintignore with ignores

The .eslintignore file is no longer read by ESLint 10. Move your ignore patterns into the config.

Legacy .eslintignore:

node_modules/
dist/
.next/
coverage/
*.min.js

Flat config ignores (in eslint.config.mjs):

export default [
  {
    ignores: [
      "node_modules/",
      "dist/",
      ".next/",
      "coverage/",
      "**/*.min.js",
    ]
  },
  // ... rest of config
];

Important: The ignores-only config object must be first in the array and must not contain any other keys (rules, plugins, etc.) to be treated as global ignores.

TypeScript Projects: typescript-eslint v8

If you're using @typescript-eslint/eslint-plugin and @typescript-eslint/parser, you need to upgrade to typescript-eslint v8 (the new consolidated package):

npm install typescript-eslint@latest --save-dev
# The old split packages are deprecated — use the unified package

Legacy:

// .eslintrc.js
module.exports = {
  parser: "@typescript-eslint/parser",
  plugins: ["@typescript-eslint"],
  extends: ["plugin:@typescript-eslint/recommended"]
}

Flat config with typescript-eslint v8:

// eslint.config.mjs
import tseslint from "typescript-eslint";

export default tseslint.config(
  ...tseslint.configs.recommended,
  {
    languageOptions: {
      parserOptions: {
        project: true,
        tsconfigRootDir: import.meta.dirname,
      },
    },
    rules: {
      "@typescript-eslint/no-unused-vars": "error",
      "@typescript-eslint/no-explicit-any": "warn",
    }
  }
);

The tseslint.config() wrapper is a convenience helper that adds type safety to your config. It's optional but recommended — it validates your config shape and provides autocompletion in editors.

Next.js Projects: eslint-config-next

eslint-config-next has an open ESLint 10 compatibility issue as of April 2026. The package does not yet declare ESLint 10 in its peer dependencies, causing install conflicts. Track the fix at vercel/next.js#91702.

Until Vercel ships an update, install ESLint 10 with the legacy peer deps flag:

npm install eslint@10 --save-dev --legacy-peer-deps

A working flat config for a Next.js project while waiting for official support:

// eslint.config.mjs
import { FlatCompat } from "@eslint/eslintrc";
import js from "@eslint/js";
import { fileURLToPath } from "url";
import path from "path";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const compat = new FlatCompat({ baseDirectory: __dirname });

export default [
  js.configs.recommended,
  ...compat.extends("next/core-web-vitals"),
  {
    ignores: [".next/**", "node_modules/**"],
  },
];

FlatCompat from @eslint/eslintrc bridges the legacy extends syntax into flat config. It's a workaround until eslint-config-next ships native flat config support.

Then verify Next.js linting still works:

npx next lint

Common Migration Errors

Error: TypeError: Failed to load config "plugin:react/recommended"

You have a legacy config still being loaded somewhere. Check:

  • package.json for an eslintConfig key
  • That you've deleted all .eslintrc.* files
  • That you haven't accidentally set ESLINT_USE_FLAT_CONFIG=false in scripts

Error: TypeError: context.getScope is not a function

A plugin you're using isn't compatible with ESLint 10's flat config API. Check the plugin's issue tracker for a flat-config-compatible version. Common culprits: older versions of eslint-plugin-import, eslint-plugin-jsx-a11y.

Error: Parsing error: Cannot read file '…/tsconfig.json'

The tsconfigRootDir in parserOptions needs to point to the directory containing tsconfig.json. In ESM configs, use import.meta.dirname. In CJS configs, use __dirname.

Rules aren't applying to .ts files

Flat config doesn't auto-detect TypeScript. Add explicit file patterns:

{
  files: ["**/*.ts", "**/*.tsx"],
  rules: {
    // TypeScript-specific rules here
  }
}

Updating npm Scripts

After migration, update your package.json lint scripts:

{
  "scripts": {
    "lint": "eslint .",
    "lint:fix": "eslint . --fix",
    "lint:ci": "eslint . --max-warnings 0"
  }
}

Remove any ESLINT_USE_FLAT_CONFIG=false environment variables from scripts — they no longer have effect in ESLint 10 and will cause warnings.

ESLint 10 New Features Worth Knowing

Beyond the config breaking change, ESLint 10 ships several improvements:

  • --flag for experimental features — stable mechanism for opting into preview features without modifying config
  • JSX reference tracking — JSX elements are now tracked as variable references. no-unused-vars correctly reports unused components; rules like @eslint-react/jsx-uses-vars or react/jsx-uses-vars that worked around this gap can be removed from your config
  • Faster rule execution — internal caching improvements reduce re-lint time on large codebases by ~15-20% in benchmarks
  • node: protocol support — Node.js built-in imports using node:fs, node:path syntax are now correctly recognized

Plugin Ecosystem Migration Status (2026)

The majority of widely-used ESLint plugins now support flat config. The migration blockers that existed in 2024 and early 2025 have largely been resolved:

Fully migrated (flat config native or supported):

  • typescript-eslint v8+ — flat config native, the recommended setup
  • eslint-plugin-react v7.37+ — flat config support added
  • eslint-plugin-react-hooks v5+ — updated for flat config
  • eslint-config-next v14.1+ — updated, see the Next.js section above
  • eslint-plugin-import (maintained fork: eslint-plugin-import-x) — flat config native
  • eslint-plugin-jsx-a11y — flat config support added in v6.10
  • eslint-plugin-unicorn — flat config native since v54

Still on legacy config (check before upgrading):

  • Some organization-specific internal plugins that haven't been updated
  • Older eslint-plugin-* packages pinned at legacy versions in lock files

If your codebase uses eslint-plugin-import (the original, unmaintained fork), migrate to eslint-plugin-import-x first — it's a maintained drop-in replacement with flat config support. The original package has been effectively abandoned.

For any plugin not yet migrated, the @eslint/compat package provides a fixupConfigRules() utility that wraps legacy rules for use in flat config without requiring a rewrite of the plugin itself. This is the correct bridge solution for plugins that work fine but haven't shipped flat config wrappers:

import { fixupConfigRules } from "@eslint/compat";
import legacyPlugin from "eslint-plugin-legacy-thing";

export default [
  ...fixupConfigRules(legacyPlugin.configs.recommended),
];

The fixupConfigRules approach works for the vast majority of plugins, making the "incompatible plugin" blocking case much rarer in 2026 than it was at ESLint 10's release.

Team Rollout Strategy

Migrating ESLint across a team with an active codebase requires coordination if your project runs lint checks in CI. The cleanest approach is to migrate in one PR, since flat config and legacy config cannot coexist in the same project. A partial migration creates an inconsistent lint experience where some developers run the old config and others run the new one.

Before starting the migration PR, communicate to the team that ESLint rules may behave slightly differently after the migration — this is expected and not a bug. Some rules that were implicitly disabled through complex extends chains may surface as active; some that appeared active may no longer match. Run a full lint pass after the migration and address any new errors as part of the migration PR rather than leaving them for follow-up, since rule changes discovered later are harder to associate with the migration as the cause.

For monorepos, the recommended approach is to migrate the root ESLint config first, then verify that workspace-level configs correctly extend the root. Flat config's extends replacement uses JavaScript array.concat() patterns — workspace configs should export arrays that merge the root config array with workspace-specific overrides, not standalone flat config objects.

The migration time for most projects is 1-3 hours for initial setup and 1-2 hours of follow-up for any new rule violations surfaced. Budgeting a full day for a large monorepo with many plugins is appropriate.

Staying on ESLint 9 (If You Must)

If your migration is blocked by an incompatible plugin, ESLint 9 remains LTS-supported. The ESLINT_USE_FLAT_CONFIG=false flag works in ESLint 9 to keep legacy config loading:

# package.json scripts
"lint": "ESLINT_USE_FLAT_CONFIG=false eslint ."

This is a temporary workaround. ESLint 9 will eventually reach end-of-life, and plugin maintainers are actively releasing flat-config-compatible versions. The longer you defer migration, the larger the diff becomes.

Check plugin compatibility at the ESLint flat config compatibility tracker before deciding to defer.


Related comparisons: ESLint vs Biome 2026, OXLint vs ESLint 2026, ESLint vs Biome vs OXLint JavaScript Linting.

See package health and download trends: eslint on PkgPulse, typescript-eslint on PkgPulse.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.