Skip to main content
doc0blade47/next.jsdocumentation home
Sign in

ESLint Rules

Last updated Oct 12, 2026100% grounded
Relevant source files

The following files were used as context for generating this wiki page:

Overview

The Next.js ESLint integration provides specialized linting rules through [[ESLint Rules]] in packages like @next/eslint-plugin-next and [[eslint-config-next]] to enforce framework best practices, optimize performance packages/eslint-plugin-next/src/rules/no-img-element.ts:7-14, and prevent common configuration errors packages/eslint-plugin-next/src/index.ts:26-51. By validating static Abstract Syntax Trees (AST) against expected Next.js conventions, these rules catch improper component imports, suboptimal asset loading, and incorrect document structure at development time [packages/eslint-plugin-next/src/rules/no-page-custom-font.ts:12-21, packages/eslint-plugin-next/src/index.ts:18-22. The plugin exports predefined configurations such as recommended and core-web-vitals that seamlessly integrate with standard and flat ESLint setups to maintain runtime efficiency and Core Web Vitals compliance packages/eslint-plugin-next/src/index.ts:90-124. Sources: packages/eslint-plugin-next/src/rules/no-img-element.ts:7-14, packages/eslint-plugin-next/src/index.ts:26-51, packages/eslint-plugin-next/src/rules/no-page-custom-font.ts:12-21, packages/eslint-plugin-next/src/index.ts:18-22, packages/eslint-plugin-next/src/index.ts:90-124

Plugin Architecture and Rule Registration

Rule Index Registration

The @next/eslint-plugin-next package centralizes its diagnostic rules by importing individual rule modules and registering them under the rules dictionary of the exported plugin object. The plugin exposes 24 distinct rules covering font attributes, script boundaries, document structure, and asset optimizations. These rules are categorized into warning-level recommendations, error-level restrictions, and core web vitals strict assertions.

const plugin = {
  meta: {
    name: '@next/eslint-plugin-next',
  },
  rules: {
    'google-font-display': googleFontDisplay,
    'google-font-preconnect': googleFontPreconnect,
    'inline-script-id': inlineScriptId,
    'next-script-for-ga': nextScriptForGa,
    'no-assign-module-variable': noAssignModuleVariable,
    'no-async-client-component': noAsyncClientComponent,
    'no-before-interactive-script-outside-document':
      noBeforeInteractiveScriptOutsideDocument,
    'no-css-tags': noCssTags,
    'no-document-import-in-page': noDocumentImportInPage,
    'no-duplicate-head': noDuplicateHead,
    'no-head-element': noHeadElement,
    'no-head-import-in-document': noHeadImportInDocument,
    'no-html-link-for-pages': noHtmlLinkForPages,
    'no-img-element': noImgElement,
    'no-location-assign-relative-destination': noLocationAssign,
    'no-page-custom-font': noPageCustomFont,
    'no-script-component-in-head': noScriptComponentInHead,
    'no-styled-jsx-in-document': noStyledJsxInDocument,
    'no-sync-scripts': noSyncScripts,
    'no-title-in-document-head': noTitleInDocumentHead,
    'no-typos': noTypos,
    'no-unwanted-polyfillio': noUnwantedPolyfillio,
  } satisfies Record<string, Rule.RuleModule>,
  configs: {} as ESLintPluginConfigs,
}

Sources: packages/eslint-plugin-next/src/index.ts:58-88

Configuration Profiles and Legacy Compatibility

The plugin defines both modern flat configurations and legacy configuration objects via Object.assign on plugin.configs. The recommended configuration applies standard framework warnings and errors, while core-web-vitals merges recommendedRules with strict performance assertions for metrics such as unoptimized anchor links and synchronous scripts.

Configuration NameTarget EnvironmentIncluded Rules Source
recommended-legacyLegacy ESLint (plugins: ['@next/next'])recommendedRules
core-web-vitals-legacyLegacy ESLint (extends recommended-legacy)recommendedRules + coreWebVitalsRules
recommendedModern Flat Config (plugins: { '@next/next': plugin })recommendedRules
core-web-vitalsModern Flat ConfigrecommendedRules + coreWebVitalsRules

Sources: packages/eslint-plugin-next/src/index.ts:90-124

Note

The core-web-vitals configuration overrides specific rules like @next/next/no-html-link-for-pages and @next/next/no-sync-scripts from warning severity to strict errors to protect loading performance metrics.

Sources: packages/eslint-plugin-next/src/index.ts:53-56, packages/eslint-plugin-next/src/index.ts:114-123

Shared Configuration and Base Setup

The [[eslint-config-next]] package coordinates multiple third-party parser and plugin integrations alongside @next/eslint-plugin-next. It establishes global file matching patterns for JavaScript and TypeScript source files (**/*.{js,jsx,mjs,ts,tsx,mts,cts}), configures Babel parsing settings with top-level await support, and registers resolvers for eslint-plugin-import.

const config: Linter.Config[] = [
  {
    name: 'next',
    files: ['**/*.{js,jsx,mjs,ts,tsx,mts,cts}'],
    plugins: {
      react: fixupPluginRules(react),
      'react-hooks': reactHooks as ESLint.Plugin,
      import: fixupPluginRules(importPlugin),
      'jsx-a11y': jsxA11yPlugin,
      '@next/next': next,
    },
    languageOptions: {
      parser: eslintParser,
      parserOptions: {
        requireConfigFile: false,
        sourceType: 'module',
        allowImportExportEverywhere: true,
        babelOptions: {
          presets: ['next/babel'],
          caller: {
            supportsTopLevelAwait: true,
          },
        },
      },
      globals: {
        ...globals.browser,
        ...globals.node,
      },
    },
    settings: {
      react: {
        version: 'detect',
      },
      'import/parsers': {
        '@typescript-eslint/parser': ['.ts', '.mts', '.cts', '.tsx', '.d.ts'],
      },
      'import/resolver': {
        [require.resolve('eslint-import-resolver-node')]: {
          extensions: ['.js', '.jsx', '.ts', '.tsx'],
        },
        [require.resolve('eslint-import-resolver-typescript')]: {
          alwaysTryTypes: true,
        },
      },
    },
    rules: {
      ...react.configs.recommended.rules,
      ...reactHooks.configs.recommended.rules,
      ...next.configs.recommended.rules,
      'import/no-anonymous-default-export': 'warn',
      'react/no-unknown-property': 'off',
      'react/react-in-jsx-scope': 'off',
      'react/prop-types': 'off',
      'jsx-a11y/alt-text': [
        'warn',
        {
          elements: ['img'],
      img: ['Image'],
        },
      ],
      'jsx-a11y/aria-props': 'warn',
      'jsx-a11y/aria-proptypes': 'warn',
      'jsx-a11y/aria-unsupported-elements': 'warn',
      'jsx-a11y/role-has-required-aria-props': 'warn',
      'jsx-a11y/role-supports-aria-props': 'warn',
      'react/jsx-no-target-blank': 'off',
    },
  },
  {
    name: 'next/typescript',
    files: ['**/*.ts', '**/*.tsx'],
    plugins: {
      '@typescript-eslint': tsEslint.plugin,
    },
    languageOptions: {
      parser: tsEslint.parser,
      parserOptions: {
        sourceType: 'module',
      },
    },
  },
  {
    ignores: [
      '.next/**',
      'out/**',
      'build/**',
      'next-env.d.ts',
    ],
  },
]

Sources: packages/eslint-config-next/src/index.ts:17-117

Warning

Configuration modules for eslint-config-next and its core web vitals extension export using CommonJS export = config syntax rather than ES module export default to maintain compatibility with legacy ESLint parser loading mechanisms.

Sources: packages/eslint-config-next/src/index.ts:119-121, packages/eslint-config-next/src/core-web-vitals.ts:10-12

Script Optimization and Polyfill Detection

Script Loading and Placement Rules

Lint rules in eslint-plugin-next enforce optimal script loading strategies, mandate unique identifiers for inline scripts, restrict third-party polyfill injections, and flag suboptimal Google Analytics or Tag Manager implementations.

The no-sync-scripts rule intercepts raw HTML <script> tags that specify a src attribute without accompanying async or defer properties, protecting page load performance from render-blocking network requests. Sources: packages/eslint-plugin-next/src/rules/no-sync-scripts.ts:17-37

The no-script-component-in-head rule traverses the AST to detect next/script (<Script>) components nested inside next/head (<Head>), reporting an error because script loading strategies must operate outside of document head tags. Sources: packages/eslint-plugin-next/src/rules/no-script-component-in-head.ts:27-53

The no-before-interactive-script-outside-document rule ensures that next/script instances utilizing the beforeInteractive loading strategy are confined to pages/_document.js when working within the pages router architecture, automatically bypassing checks when code resides inside the app/ directory. Sources: packages/eslint-plugin-next/src/rules/no-before-interactive-script-outside-document.ts:30-69

Warning

The no-before-interactive-script-outside-document check inspects file paths via context.filename, converting slashes using path.sep to verify if the file sits in an app directory, and skips validation entirely for App Router components. Sources: packages/eslint-plugin-next/src/rules/no-before-interactive-script-outside-document.ts:31-38

Inline Script Validation and Third-Party Integrations

The inline-script-id rule verifies that any next/script component containing inline children or a dangerouslySetInnerHTML attribute explicitly defines an id attribute, ignoring elements with uncheckable JSX spread attributes. Sources: packages/eslint-plugin-next/src/rules/inline-script-id.ts:25-72

The next-script-for-ga rule detects raw script tags or inline blocks loading Google Analytics or Google Tag Manager URLs, prompting developers to adopt @next/third-parties/google instead. Sources: packages/eslint-plugin-next/src/rules/next-script-for-ga.ts:28-80

Integration TargetURL Signatures CheckedRecommendation Message
Google Analyticswww.google-analytics.com/analytics.jsPrefer GoogleAnalytics component from @next/third-parties/google when using the inline script for Google Analytics.
Google Tag Managerwww.googletagmanager.com/gtag/js, www.googletagmanager.com/gtm.jsPrefer GoogleTagManager component from @next/third-parties/google when using the inline script for Google Tag Manager.

Sources: packages/eslint-plugin-next/src/rules/next-script-for-ga.ts:4-8, packages/eslint-plugin-next/src/rules/next-script-for-ga.ts:13-14

The no-unwanted-polyfillio rule inspects script sources matching recognized Polyfill.io and CDN endpoints, parsing requested query features against the built-in Next.js polyfill manifest to prevent redundant network payloads. Sources: packages/eslint-plugin-next/src/rules/no-unwanted-polyfillio.ts:97-141

if (
  src.startsWith('https://cdn.polyfill.io/v2/') ||
  src.startsWith('https://polyfill.io/v3/') ||
  // https://community.fastly.com/t/new-options-for-polyfill-io-users/2540
  src.startsWith('https://polyfill-fastly.net/') ||
  src.startsWith('https://polyfill-fastly.io/') ||
  // https://blog.cloudflare.com/polyfill-io-now-available-on-cdnjs-reduce-your-supply-chain-risk
  src.startsWith('https://cdnjs.cloudflare.com/polyfill/')
) {

Sources: packages/eslint-plugin-next/src/rules/no-unwanted-polyfillio.ts:116-124

Note

The no-unwanted-polyfillio rule checks requests originating from https://cdn.polyfill.io/v2/, https://polyfill.io/v3/, Fastly mirrors (https://polyfill-fastly.net/, https://polyfill-fastly.io/), and Cloudflare CDN paths (https://cdnjs.cloudflare.com/polyfill/). Sources: packages/eslint-plugin-next/src/rules/no-unwanted-polyfillio.ts:116-124

Document and Head Tag Rules

Head Component and Document Structure Rules

The eslint-plugin-next package provides AST validation rules to ensure correct usage of document-level tags and components within the Pages router architecture. These rules prevent misuse of native HTML elements like <head> in pages, restrict next/document imports and stylesheet tags, and protect against duplicate Head declarations or forbidden elements in custom documents.

Rule NameDescriptionTarget File Context
no-head-elementPrevent usage of native <head> element, recommending next/head instead.Pages router files (bypasses app/)
no-document-import-in-pagePrevent importing next/document outside of pages/_document.js.Any file outside pages/_document.js
no-head-import-in-documentPrevent usage of next/head in pages/_document.js.pages/_document.js or pages/_document/index.js
no-title-in-document-headPrevent usage of <title> with Head from next/document.Files importing Head from next/document
no-styled-jsx-in-documentPrevent usage of styled-jsx style tags in pages/_document.js.pages/_document.js or pages/_document/index.js
no-duplicate-headPrevent duplicate usage of <Head> in pages/_document.js.pages/_document.js class components
no-css-tagsPrevent manual local stylesheet <link rel="stylesheet"> tags.Any JSX element

Sources: packages/eslint-plugin-next/src/rules/no-head-element.ts:7-16, packages/eslint-plugin-next/src/rules/no-document-import-in-page.ts:7-15, packages/eslint-plugin-next/src/rules/no-head-import-in-document.ts:7-15, packages/eslint-plugin-next/src/rules/no-title-in-document-head.ts:5-14, packages/eslint-plugin-next/src/rules/no-styled-jsx-in-document.ts:6-15, packages/eslint-plugin-next/src/rules/no-duplicate-head.ts:5-14, packages/eslint-plugin-next/src/rules/no-css-tags.ts:5-13

Warning

The no-head-element rule checks file paths to determine if code resides within the app directory using path.sep or path.posix.sep, and completely bypasses validation for App Router components. Sources: packages/eslint-plugin-next/src/rules/no-head-element.ts:20-28

Path Inspection and Document Boundary Enforcement

Rules inspecting pages/_document.js parse the filename via Node's path module to verify exact matching against _document names or /_document/index directory structures.

const { name, dir } = path.parse(document)
 
if (
  name.startsWith('_document') ||
  (dir === '/_document' && name === 'index')
) {

Sources: packages/eslint-plugin-next/src/rules/no-head-import-in-document.ts:28-33

Note

The no-css-tags rule targets JSX opening link elements, filtering for attributes where rel equals stylesheet and href is a literal string not matching an absolute protocol via /^https?/. Sources: packages/eslint-plugin-next/src/rules/no-css-tags.ts:16-37

Font Optimization and External Preconnects

Google Font Validation Rules

The eslint-plugin-next package provides static AST rules to enforce performance best practices when loading Google Fonts via <link> elements. These rules inspect JSX opening elements with the name link to verify correct attributes, preconnect linkages, and font-display parameters.

Rule NameDescriptionTarget URL / Behavior
google-font-displayEnforces font-display parameters on Google Font stylesheet links.https://nextjs.org/docs/messages/google-font-display
google-font-preconnectEnsures rel="preconnect" is present when loading fonts.gstatic.com.https://nextjs.org/docs/messages/google-font-preconnect
no-page-custom-fontPrevents page-only custom Google Font CSS links outside pages/_document.js.https://nextjs.org/docs/messages/no-page-custom-font

Sources: packages/eslint-plugin-next/src/rules/google-font-display.ts:6-15, packages/eslint-plugin-next/src/rules/google-font-preconnect.ts:6-15, packages/eslint-plugin-next/src/rules/no-page-custom-font.ts:12-21

Warning

The google-font-display rule parses the query string of any https://fonts.googleapis.com/css link. If the display parameter is missing, or if its value is set to auto, block, or fallback, the rule reports a violation. Sources: packages/eslint-plugin-next/src/rules/google-font-display.ts:31-50

Preconnect Inspection and Custom Font Scope

The google-font-preconnect rule checks whether link tags loading from https://fonts.gstatic.com have their rel attribute correctly configured with the value preconnect.

const hrefValue = attributes.value('href')
const preconnectMissing =
  !attributes.has('rel') ||
  !attributes.hasValue('rel') ||
  attributes.value('rel') !== 'preconnect'
 
if (
  typeof hrefValue === 'string' &&
  hrefValue.startsWith('https://fonts.gstatic.com') &&
  preconnectMissing
) {

Sources: packages/eslint-plugin-next/src/rules/google-font-preconnect.ts:28-38

Note

The no-page-custom-font rule validates files residing inside the pages directory. It checks whether https://fonts.googleapis.com/css links are placed within pages/_document.js and inside the default export component, reporting a warning if custom fonts are scoped only to individual pages. Sources: packages/eslint-plugin-next/src/rules/no-page-custom-font.ts:22-35, packages/eslint-plugin-next/src/rules/no-page-custom-font.ts:142-167

Routing, Navigation, and Image Rules

Routing and Navigation Rules

The eslint-plugin-next package implements static analysis rules to discourage unoptimized markup and raw browser location transitions in favor of framework-managed alternatives. These checks target native JSX elements and imperative window navigation properties.

Rule NameDescriptionTarget Documentation URL
no-img-elementPrevent usage of <img> element due to slower LCP and higher bandwidth.https://nextjs.org/docs/messages/no-img-element
no-html-link-for-pagesPrevent usage of <a> elements to navigate to internal Next.js pages.https://nextjs.org/docs/messages/no-html-link-for-pages
no-location-assign-relative-destinationPrevent usage of location.assign or location.href assignment to navigate to internal Next.js pages.https://nextjs.org/docs/messages/no-location-assign-relative-destination

Sources: packages/eslint-plugin-next/src/rules/no-img-element.ts:4-16, packages/eslint-plugin-next/src/rules/no-html-link-for-pages.ts:38-66, packages/eslint-plugin-next/src/rules/no-location-assign-relative-destination.ts:10-28

Warning

The no-img-element rule checks JSX opening elements named img. It ignores elements without attributes, elements wrapped in a parent <picture> element, and metadata route files such as opengraph-image.js, twitter-image.js, or icon.js within the App Directory. Sources: packages/eslint-plugin-next/src/rules/no-img-element.ts:25-47

The no-html-link-for-pages rule verifies all anchor (<a>) elements against known routes derived from the filesystem's pages and app directories. Outgoing absolute links, links with a download attribute, and links with target="_blank" are automatically exempted from validation.

if (target && target.value.value === '_blank') {
  return
}
 
const href = node.attributes.find(
  (attr) => attr.type === 'JSXAttribute' && attr.name.name === 'href'
)
 
if (!href || (href.value && href.value.type !== 'Literal')) {
  return
}
 
const hasDownloadAttr = node.attributes.find(
  (attr) =>
    attr.type === 'JSXAttribute' && attr.name.name === 'download'
)
 
if (hasDownloadAttr) {
  return
}
 
const hrefPath = normalizeURL(href.value.value)
// Outgoing links are ignored
if (/^(https?:\/\/|\/\/)/.test(hrefPath)) {
  return
}
 

Sources: packages/eslint-plugin-next/src/rules/no-html-link-for-pages.ts:128-154

Note

The no-location-assign-relative-destination rule inspects CallExpression nodes matching assign and AssignmentExpression nodes targeting href on global identifiers including window, globalThis, document, and self. It resolves static string prefixes, template literals, and variable assignments to detect relative URL destinations. Sources: packages/eslint-plugin-next/src/rules/no-location-assign-relative-destination.ts:39-80, packages/eslint-plugin-next/src/rules/no-location-assign-relative-destination.ts:85-86, packages/eslint-plugin-next/src/rules/no-location-assign-relative-destination.ts:133-169

Component Boundaries and Migration Codemods

Component Boundary Rules and Internal Checks

The eslint-plugin-next package includes static analysis rules to enforce architectural constraints at React component boundaries. Specifically, the no-async-client-component rule flags Client Components that are declared as asynchronous functions. It inspects programs for the presence of a 'use client' directive statement followed by an export default declaration or an exported identifier referencing an asynchronous function declaration or an arrow function expression whose name begins with an uppercase letter.

Additionally, internal validation plugins such as eslint-plugin-internal provide auxiliary AST checks like no-ambiguous-jsx. This rule identifies unbundled JSX fragments (JSXFragment) and opening elements (JSXOpeningElement), reporting ambiguous usage that would otherwise default to React Client when not bundled in next-server.

Rule / PluginTarget AST NodesViolation Condition
no-async-client-componentProgram, ExportDefaultDeclaration, FunctionDeclaration, VariableDeclarationuse client directive present alongside an async default function or variable declaration with a capitalized identifier.
no-ambiguous-jsxJSXOpeningElement, JSXFragmentUnbundled JSX syntax not routed through framework compilation modules for React Server Components.

Sources: packages/eslint-plugin-next/src/rules/no-async-client-component.ts:22-108, packages/eslint-plugin-internal/src/eslint-no-ambiguous-jsx.js:19-31

Warning

The no-async-client-component rule evaluates identifier capitalization via /[A-Z]/.test(str?.[0]) to distinguish React components from standard helper functions, reporting violations directly on the matching function declaration or variable declarator node. Sources: packages/eslint-plugin-next/src/rules/no-async-client-component.ts:7-9, packages/eslint-plugin-next/src/rules/no-async-client-component.ts:76-102

Next Lint Flat Config CLI Migration

The next-lint-to-eslint-cli codemod utility transforms legacy ESLint configurations and FlatCompat usages into native ESLint v9 flat configuration structures. It checks project roots for existing flat or legacy configuration files, parses configuration syntax with jscodeshift, and extracts extended Next.js rulesets from compat.extends() or compat.config() calls.

function findExistingEslintConfig(projectRoot: string): {
  exists: boolean
  path: string | null
  isLegacy: boolean | null
} {
  const flatConfigs = [
    'eslint.config.js',
    'eslint.config.mjs',
    'eslint.config.cjs',
    'eslint.config.ts',
    'eslint.config.mts',
    'eslint.config.cts',
  ]
 
  const legacyConfigs = [
    '.eslintrc.js',
    '.eslintrc.cjs',
    '.eslintrc.yaml',
    '.eslintrc.yml',
    '.eslintrc.json',
    '.eslintrc',
  ]
 
  // Check for flat configs first (preferred for v9+)
  for (const config of flatConfigs) {
    const configPath = path.join(projectRoot, config)
    if (existsSync(configPath)) {
      return { exists: true, path: configPath, isLegacy: false }
    }
  }
 
  // Check for legacy configs
  for (const config of legacyConfigs) {
    const configPath = path.join(projectRoot, config)
    if (existsSync(configPath)) {
      return { exists: true, path: configPath, isLegacy: true }
    }
  }
 
  return { exists: false, path: null, isLegacy: null }
}

Sources: packages/next-codemod/transforms/next-lint-to-eslint-cli.ts:66-106

Note

During configuration transformation, the codemod detects project TypeScript capability via tsconfig.json existence to select between ESLINT_CONFIG_TEMPLATE_TYPESCRIPT and ESLINT_CONFIG_TEMPLATE_JAVASCRIPT, automatically inserting standard ignore patterns for node_modules/**, .next/**, out/**, build/**, and next-env.d.ts. Sources: packages/next-codemod/transforms/next-lint-to-eslint-cli.ts:24-64, packages/next-codemod/transforms/next-lint-to-eslint-cli.ts:132-164

Was this page helpful?