<!-- PkgPulse AI-readable guide source -->
<!-- Canonical: https://www.pkgpulse.com/guides/eslint-10-flat-config-migration-guide-2026 -->
<!-- Raw Markdown: https://www.pkgpulse.com/guides/eslint-10-flat-config-migration-guide-2026/raw.md -->
<!-- Source path: content/guides/eslint-10-flat-config-migration-guide-2026.mdx -->

---
og_image: "/images/guides/eslint-10-flat-config-migration-guide-2026.webp"
title: "ESLint 10 Flat Config Migration Guide 2026"
description: "ESLint 10 (Feb 2026) removes legacy .eslintrc for good. Migrate to eslint.config.js in under an hour: steps, TypeScript setup, and Next.js compatibility fix."
date: "2026-04-09"
author: "PkgPulse Team"
tags: ["eslint", "eslint-10", "flat-config", "migration", "javascript", "typescript", "linting", "2026"]
tier: 1
---

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

| Feature | Legacy `.eslintrc` | Flat Config `eslint.config.js` |
|---|---|---|
| File format | JSON / YAML / JS | JavaScript only |
| Config loading | Cascading (directory hierarchy) | Single flat array |
| Plugin loading | `plugins: ["react"]` (string) | `import reactPlugin from 'eslint-plugin-react'` |
| Extends | `extends: ["airbnb"]` | Spread plugin configs directly |
| Ignore file | `.eslintignore` | `ignores: [...]` in config |
| `env` key | `env: { browser: true }` | `globals` from `globals` package |
| TypeScript config | `@typescript-eslint/parser` in `parser` field | `tseslint.config(...)` wrapper |
| ESLint version | 1–9 | 8–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

```bash
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:

```bash
npm install eslint@9 --save-dev
```

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

```bash
npm install eslint@10 --save-dev
```

## Step 2: Run the ESLint Migration Helper

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

```bash
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:**
```js
// .eslintrc.js
module.exports = {
  extends: [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:react/recommended",
    "next/core-web-vitals"
  ]
}
```

**Flat config:**
```js
// 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.

```bash
npm install globals --save-dev
```

**Legacy:**
```js
env: {
  browser: true,
  node: true,
  es2022: true
}
```

**Flat config:**
```js
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`):**
```js
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):

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

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

**Flat config with typescript-eslint v8:**
```js
// 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](https://github.com/vercel/next.js/issues/91702).

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

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

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

```js
// 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:

```bash
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:

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

## Updating npm Scripts

After migration, update your `package.json` lint scripts:

```json
{
  "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:

```js
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:

```bash
# 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](https://github.com/eslint/eslint/issues/18391) before deciding to defer.

---

*Related comparisons: [ESLint vs Biome 2026](/guides/biome-vs-eslint-vs-oxlint-2026), [OXLint vs ESLint 2026](/guides/oxlint-vs-eslint-2026), [ESLint vs Biome vs OXLint JavaScript Linting](/guides/biome-vs-eslint-vs-oxlint-2026).*

*See package health and download trends: [eslint on PkgPulse](https://www.pkgpulse.com/packages/eslint), [typescript-eslint on PkgPulse](https://www.pkgpulse.com/packages/typescript-eslint).*
