<Notes of dev/>
ESLintNextJS

ESLint Flat Config for Next.js 16: The Complete Setup Guide

April 15, 20266 min read
Person with headphones coding in a neon-lit room, rainy city view, console, and cat. Cyber café sign and vibrant decor around.

Upgraded to Next.js 16 and your linting just… stopped working? Yeah, same. Here's everything that broke for me and how I fixed it — so you don't have to waste a whole afternoon like I did.

What Broke (and Why)

Three things went wrong at the same time after upgrading to Next.js 16 (I'm using 16.2.3):

next lint is gone. Next.js 16 completely removed the built-in next lint command. If your package.json still has "lint": "next lint", it will silently fail or throw an error. The next build command no longer runs linting automatically either.

FlatCompat stopped working. If you were using @eslint/eslintrc's FlatCompat to bridge your old eslint-config-next into ESLint 9 flat config — that shim layer broke when eslint-config-next hit peer dependency conflicts with Next.js 16. FlatCompat was always meant as a migration tool, not a permanent solution.

ajv version mismatch. A loose pnpm override ("ajv@<6.14.0": ">=6.14.0") allowed ajv to resolve to v8.x. ESLint 9 depends on ajv v6 internally, and v8 has a completely different API. The result? Cryptic schema compilation errors that never mention ajv by name.

The Fix: Native Flat Config (No Shims)

The solution is to ditch FlatCompat and eslint-config-next entirely, and import each plugin directly.

Step 1 — Install Dependencies

Remove the old packages first:

pnpm remove @eslint/eslintrc eslint-config-next

Then install the replacements:

pnpm add -D -E eslint@latest typescript-eslint eslint-plugin-react eslint-plugin-react-hooks @next/eslint-plugin-next

Optional extras (add what you need):

# Accessibility
pnpm add -D -E eslint-plugin-jsx-a11y

# Code style
pnpm add -D -E @stylistic/eslint-plugin

# Tailwind CSS (v4 beta)
pnpm add -D -E eslint-plugin-better-tailwindcss@beta

# MDX support
pnpm add -D -E eslint-plugin-mdx

# React Compiler (experimental)
pnpm add -D -E eslint-plugin-react-compiler

Step 2 — Create eslint.config.mjs

Delete any old .eslintrc.* files and create eslint.config.mjs in your project root:

import { defineConfig } from "eslint/config";
import { configs as tseslintConfigs } from "typescript-eslint";
import reactPlugin from "eslint-plugin-react";
import reactHooksPlugin from "eslint-plugin-react-hooks";
import nextPlugin from "@next/eslint-plugin-next";

export default defineConfig([
  // 1. Global ignores — must be its own config object
  {
    name: "project/ignores",
    ignores: [".next/", "node_modules/", "public/", ".vscode/"],
  },

  // 2. TypeScript rules
  {
    name: "project/typescript",
    files: ["**/*.{ts,tsx}"],
    extends: [tseslintConfigs.recommended],
    languageOptions: {
      parserOptions: {
        projectService: true,
        tsconfigRootDir: import.meta.dirname,
      },
    },
  },

  // 3. React + Next.js rules
  {
    name: "project/react-next",
    files: ["**/*.{js,jsx,ts,tsx,mjs}"],
    plugins: {
      react: reactPlugin,
      "react-hooks": reactHooksPlugin,
      "@next/next": nextPlugin,
    },
    rules: {
      ...reactPlugin.configs.recommended.rules,
      ...reactPlugin.configs["jsx-runtime"].rules,
      ...reactHooksPlugin.configs["recommended-latest"].rules,
      ...nextPlugin.configs.recommended.rules,
      ...nextPlugin.configs["core-web-vitals"].rules,
      "react/prop-types": "off", // Using TypeScript instead
    },
    settings: {
      react: { version: "detect" },
    },
  },
]);

That's the minimal working config. Each plugin's rules are spread explicitly — verbose, but you can see exactly what's active. No magic, no shim layer.

Step 3 — Update package.json Scripts

{
  "scripts": {
    "dev": "next dev",
    "build": "pnpm lint && next build",
    "start": "next start",
    "lint": "eslint --cache --cache-location .next/cache/eslint/",
    "lint:fix": "eslint --fix"
  }
}

Key changes: next linteslint, and the build script explicitly runs lint first since Next.js won't do it for you anymore.

Step 4 — Fix the "ajv" Override (if applicable)

If you have an ajv override in package.json (common for security patches), pin it tightly:

{
  "pnpm": {
    "overrides": {
      "ajv@<6.14.0": "~6.14.0"
    }
  }
}

The ~ keeps it on 6.14.x. Using >= lets pnpm resolve to v8, which will break ESLint silently.

Step 5 — Clean Up next.config.mjs

If your Next config had an eslint option, remove it — it's no longer supported:

const nextConfig = {
  // Remove this — no longer supported in Next.js 16
  // eslint: { ignoreDuringBuilds: true },
};

Step 6 — Run It

pnpm lint

If everything is wired up correctly, ESLint should run cleanly against your source files.

Leveling Up: Accessibility, Styling, and More

The config above is the practical minimum. Here's how to add more capabilities:

Accessibility with jsx-a11y

import jsxA11yPlugin from "eslint-plugin-jsx-a11y";

// Inside your react-next config block:
plugins: {
  // ... existing plugins
  "jsx-a11y": jsxA11yPlugin,
},
rules: {
  // ... existing rules
  ...jsxA11yPlugin.configs.strict.rules,
  "jsx-a11y/alt-text": ["warn", { elements: ["img"], img: ["Image"] }],
},

Code Style with @stylistic/eslint-plugin

import stylisticPlugin from "@stylistic/eslint-plugin";

// Add as a separate config block:
{
  name: "project/stylistic",
  files: ["**/*.{js,mjs,ts,tsx}"],
  plugins: { "@stylistic": stylisticPlugin },
  rules: {
    ...stylisticPlugin.configs["disable-legacy"].rules,
    ...stylisticPlugin.configs.recommended.rules,
    "@stylistic/indent": ["warn", 2],
    "@stylistic/quotes": ["warn", "single", { avoidEscape: true }],
    "@stylistic/semi": ["warn", "never"],
  },
},

Tailwind CSS Linting

import tailwindcssPlugin from "eslint-plugin-better-tailwindcss";

{
  name: "project/tailwindcss",
  files: ["**/*.ts?(x)"],
  plugins: { "better-tailwindcss": tailwindcssPlugin },
  rules: {
    ...tailwindcssPlugin.configs.recommended.rules,
  },
  settings: {
    "better-tailwindcss": {
      entryPoint: "app/global.css",
    },
  },
},

Debugging Tips

ESLint errors are cryptic? Check your ajv version first. Run pnpm why ajv. If v8 shows up alongside ESLint, that's your problem.

Rules not applying? Use the built-in Config Inspector to see exactly what's active:

pnpm eslint --inspect-config

This opens a browser UI at localhost:7777 where you can explore every rule and which files it targets.

Want to see the resolved config for a specific file?

pnpm eslint --print-config src/app/page.tsx

Key Takeaways

  • FlatCompat is a migration tool, not a destination. Migrate to native flat config and you'll never deal with shim breakage again.
  • Loose version overrides are dangerous. >= in overrides can pull in breaking major versions silently. Use ~ unless you explicitly want that.
  • Name your config blocks. Adding name: "project/whatever" to each object makes debugging with Config Inspector so much easier.
  • Order matters. Global ignores first, then general rules, then specific overrides.