Skip to main content

How to Migrate from ESLint to Biome

·PkgPulse Team
0

TL;DR

Biome replaces ESLint + Prettier in a single tool and runs 25x faster. Biome covers ~200+ ESLint rules natively — enough for most projects. Migration is 30-60 minutes: install Biome, run biome migrate eslint, review the output, delete .eslintrc. The main limitation: Biome doesn't support custom plugins or rules from the ecosystem (eslint-plugin-react-hooks still needed for certain rules). Most teams run Biome for the 95% and keep a minimal ESLint config for the remaining plugins.

Key Takeaways

  • 25x faster than ESLint — Rust-based, processes files in parallel
  • Replaces both ESLint + Prettier — one tool for linting and formatting
  • ~200+ rules built-in — covers recommended configs for most plugins
  • No plugin system yet — third-party plugins not supported
  • biome migrate eslint — official command that auto-converts your config

Before You Start

Before installing Biome, take stock of what your current ESLint setup includes. This assessment takes five minutes and prevents surprises.

Check your ESLint version and plugins:

# Check ESLint version
npx eslint --version

# List what's installed
cat package.json | grep -E "eslint|prettier"

A typical TypeScript + React project has a package.json that looks like this in the dev dependencies section:

{
  "devDependencies": {
    "eslint": "^8.57.0",
    "@typescript-eslint/eslint-plugin": "^7.0.0",
    "@typescript-eslint/parser": "^7.0.0",
    "eslint-plugin-react": "^7.34.0",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-jsx-a11y": "^6.8.0",
    "eslint-plugin-import": "^2.29.0",
    "eslint-config-prettier": "^9.1.0",
    "prettier": "^3.2.0"
  }
}

Go through each plugin and ask: "Does Biome cover this?" Here is the answer for the most common ones:

  • @typescript-eslint — fully covered. Biome implements the core TypeScript rules natively.
  • eslint-plugin-react — mostly covered. Biome has React-specific rules for common anti-patterns.
  • eslint-plugin-react-hooks — partially covered. exhaustive-deps is covered via correctness/useExhaustiveDependencies. rules-of-hooks has a Biome equivalent but it is less comprehensive.
  • eslint-plugin-jsx-a11y — well covered. Biome's a11y rule group covers the most important accessibility rules.
  • eslint-plugin-import — partially covered. Import ordering is handled by organizeImports. Circular dependency detection (no-cycle) has no Biome equivalent.
  • prettier — fully replaced. Biome's formatter handles all of Prettier's formatting with near-identical output.
  • Custom rules — not covered. If your team has built custom ESLint rules, those have no equivalent in Biome. Keep ESLint for those.

The practical result for most projects: Biome covers everything except react-hooks/rules-of-hooks and import/no-cycle. You can run a minimal ESLint config for those two rules alongside Biome.


Step 1: Install Biome

Install Biome as a dev dependency. The --save-exact flag is important — Biome's config schema is version-pinned and upgrades can change formatting slightly, which you want to be intentional about.

npm install --save-dev --save-exact @biomejs/biome

# Initialize the config file
npx @biomejs/biome init

# This creates biome.json at the project root

The init command produces a minimal biome.json that looks like this:

{
  "$schema": "https://biomejs.dev/schemas/1.9.0/schema.json",
  "organizeImports": {
    "enabled": true
  },
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true
    }
  }
}

This is your starting point. The recommended: true flag enables a curated set of around 100 rules that cover the most important safety and correctness checks. You will expand this in the next step.


Step 2: Auto-Migrate Your ESLint Config

Biome ships an official migration command that reads your existing .eslintrc or eslint.config.js and maps known rules to their Biome equivalents:

# Auto-convert your ESLint config to Biome
npx @biomejs/biome migrate eslint --write

# This command:
# 1. Reads your .eslintrc / eslint.config.js
# 2. Maps rules to Biome equivalents
# 3. Updates biome.json with matched rules
# 4. Reports rules it couldn't migrate

# Example output:
# ✔ Migrated 45 rules
# ℹ 3 rules have no Biome equivalent:
#   - react-hooks/rules-of-hooks (use eslint-plugin-react-hooks)
#   - import/no-cycle (no equivalent)
#   - custom-rules/my-rule (custom plugin)

The --write flag applies the changes directly to biome.json. Without it, the command runs in dry-run mode and only reports what it would do.

After running, review the generated biome.json. It will be populated with the rules that Biome mapped automatically:

{
  "$schema": "https://biomejs.dev/schemas/1.9.0/schema.json",
  "organizeImports": {
    "enabled": true
  },
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true,
      "a11y": {
        "useAltText": "error",
        "noAutofocus": "warn"
      },
      "complexity": {
        "noExtraBooleanCast": "error",
        "noMultipleSpacesInRegularExpressionLiteral": "error"
      },
      "correctness": {
        "noUnusedVariables": "error",
        "useExhaustiveDependencies": "warn"
      },
      "security": {
        "noDangerouslySetInnerHtml": "warn"
      },
      "style": {
        "noVar": "error",
        "useConst": "error",
        "useSingleVarDeclarator": "error"
      },
      "suspicious": {
        "noConsoleLog": "warn",
        "noDebugger": "error",
        "noDoubleEquals": "error"
      }
    }
  },
  "formatter": {
    "enabled": true,
    "indentStyle": "space",
    "indentWidth": 2,
    "lineWidth": 80
  },
  "javascript": {
    "formatter": {
      "quoteStyle": "single",
      "trailingCommas": "es5",
      "semicolons": "always"
    }
  },
  "files": {
    "ignore": [
      "dist/**",
      "build/**",
      "node_modules/**",
      "*.min.js"
    ]
  }
}

Step 3: Run Biome on Your Codebase

Run the linter against your entire codebase and compare the output to your ESLint output:

# Check for lint and formatting issues (no auto-fix)
npx biome check .

# Auto-fix safe issues
npx biome check --apply .

# Apply unsafe fixes (changes behavior — review carefully)
npx biome check --apply-unsafe .

On the first run, you will likely see a large number of formatting differences. This is expected — Biome's formatter and Prettier produce near-identical output but with minor differences in some edge cases. Run biome format --write . to apply formatting once across the entire codebase, commit that as a standalone formatting commit, then proceed. This keeps the formatting change separate from linting changes in your git history.

# Apply Biome formatting to all files in one pass
npx biome format --write .

# Commit the formatting change separately
git add -A
git commit -m "chore: apply Biome formatting"

# Then continue with the rest of the migration

The ESLint rule → Biome rule mapping reference:

ESLint RuleBiome Equivalent
no-unused-varscorrectness/noUnusedVariables
no-varstyle/noVar
prefer-conststyle/useConst
no-consolesuspicious/noConsoleLog
no-debuggersuspicious/noDebugger
eqeqeqsuspicious/noDoubleEquals
no-extra-boolean-castcomplexity/noExtraBooleanCast
react-hooks/exhaustive-depscorrectness/useExhaustiveDependencies
jsx-a11y/alt-texta11y/useAltText
jsx-a11y/no-autofocusa11y/noAutofocus
no-dangersecurity/noDangerouslySetInnerHtml

Step 4: Replace Prettier

Biome's formatter replaces Prettier completely. If you have a .prettierrc or prettier.config.js, map its settings to biome.json formatter options and then delete the Prettier config and package.

Common Prettier options and their Biome equivalents:

// .prettierrc (before):
{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5",
  "printWidth": 80,
  "arrowParens": "always"
}

// biome.json formatter section (after):
{
  "formatter": {
    "enabled": true,
    "indentStyle": "space",
    "indentWidth": 2,
    "lineWidth": 80
  },
  "javascript": {
    "formatter": {
      "quoteStyle": "single",
      "trailingCommas": "es5",
      "semicolons": "always",
      "arrowParentheses": "always"
    }
  }
}

After updating biome.json, uninstall Prettier:

npm uninstall prettier eslint-config-prettier eslint-plugin-prettier

# Remove Prettier config files
rm .prettierrc .prettierignore prettier.config.js 2>/dev/null || true

Verify formatting still works:

npx biome format --write src/

Step 5: Update CI/CD and Git Hooks

Replace eslint and prettier invocations in your CI pipeline and git hooks with Biome equivalents.

Update package.json scripts:

{
  "scripts": {
    "lint": "biome lint .",
    "lint:fix": "biome lint --apply .",
    "format": "biome format --write .",
    "format:check": "biome format .",
    "check": "biome check --apply .",
    "ci": "biome ci ."
  }
}

The biome ci command is designed for CI environments. It runs lint and format checks with no --apply flag (fails on any issue instead of fixing it) and exits with a non-zero code if anything fails.

Update lint-staged (if you use husky):

// package.json lint-staged config
{
  "lint-staged": {
    "*.{js,ts,jsx,tsx,json}": [
      "biome check --apply --no-errors-on-unmatched"
    ]
  }
}

GitHub Actions CI workflow:

# .github/workflows/lint.yml
name: Lint & Format

on: [push, pull_request]

jobs:
  biome:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Biome
        uses: biomejs/setup-biome@v2
        with:
          version: latest

      - name: Run Biome CI
        run: biome ci .
        # biome ci exits non-zero on any lint or formatting issue

The biomejs/setup-biome@v2 GitHub Action is the official action. It downloads the Biome binary without requiring a full Node.js install, which makes it faster than npm ci + npx biome.


Step 6: Handle the Remaining ESLint Rules

For rules that Biome does not cover, keep a minimal ESLint configuration. The goal is to shrink your ESLint config to only the rules that have no Biome equivalent. Biome handles everything else.

# Keep only the plugins with no Biome equivalent
# Remove everything else
npm uninstall @typescript-eslint/eslint-plugin @typescript-eslint/parser \
  eslint-plugin-react eslint-plugin-jsx-a11y

# Keep these (they have no Biome equivalent):
# eslint-plugin-react-hooks — for rules-of-hooks
# eslint-plugin-import — for no-cycle (if you use it)
// .eslintrc.json — minimal config for rules Biome can't handle
{
  "root": true,
  "plugins": ["react-hooks"],
  "rules": {
    "react-hooks/rules-of-hooks": "error"
  },
  "ignorePatterns": ["dist/", "build/", "node_modules/"]
}

If you do not need import/no-cycle (most projects don't), you can potentially remove ESLint entirely. The react-hooks/rules-of-hooks rule is the last one most React projects need from ESLint after switching to Biome.

Run both in CI if you keep a minimal ESLint:

# CI script: run Biome first (fast), then minimal ESLint
biome ci . && eslint src/ --max-warnings 0

Editor Integration

Install the official VS Code extension for format-on-save and inline diagnostic display.

VS Code extension ID: biomejs.biome

Install it from the command line:

code --install-extension biomejs.biome

Configure VS Code to use Biome as the default formatter and disable the ESLint and Prettier extensions for this project:

// .vscode/settings.json
{
  "editor.defaultFormatter": "biomejs.biome",
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "quickfix.biome": "explicit",
    "source.organizeImports.biome": "explicit"
  },
  // Per-language formatter overrides
  "[javascript]": { "editor.defaultFormatter": "biomejs.biome" },
  "[typescript]": { "editor.defaultFormatter": "biomejs.biome" },
  "[typescriptreact]": { "editor.defaultFormatter": "biomejs.biome" },
  "[javascriptreact]": { "editor.defaultFormatter": "biomejs.biome" },
  "[json]": { "editor.defaultFormatter": "biomejs.biome" },
  "[jsonc]": { "editor.defaultFormatter": "biomejs.biome" },

  // Disable conflicting extensions
  "prettier.enable": false,
  "eslint.enable": false  // Only if you removed ESLint entirely
}

Commit this .vscode/settings.json to your repository so the whole team gets the same editor behavior automatically.


Why Teams Switch to Biome

Beyond raw speed, there are several practical reasons teams migrate from ESLint + Prettier to Biome that are worth understanding before you commit to the migration.

Configuration fatigue. A typical ESLint setup for a TypeScript React project involves eslint, @typescript-eslint/parser, @typescript-eslint/eslint-plugin, eslint-plugin-react, eslint-plugin-react-hooks, eslint-plugin-jsx-a11y, eslint-config-prettier, and Prettier itself — eight packages, multiple config files, and a fragile dependency chain where version mismatches cause cryptic errors. Biome replaces all of this with one package and one config file.

Consistency between formatting and linting. ESLint and Prettier can conflict. You need eslint-config-prettier to disable ESLint's formatting rules so they don't fight Prettier. When you add a new ESLint rule or upgrade either tool, you can reintroduce conflicts. Biome owns both the formatter and the linter, so they are always consistent by design.

Onboarding new developers. When a new developer joins a project, setting up ESLint + Prettier in VS Code requires installing multiple extensions, potentially editing workspace settings to disable conflicting formatters, and debugging "why isn't format-on-save working." Biome is one extension, one config file, and it works immediately.

Pre-commit hook performance at scale. As a codebase grows, ESLint's pre-commit hook grows slower. Teams start adding --cache flags and optimizing lint-staged configurations to avoid linting unchanged files. Biome's 0.3-second cold lint time means you never need cache optimization — it stays fast regardless of project size.

The migration is not appropriate for every team. If you have heavy investment in custom ESLint rules, or if your tooling depends on ESLint's plugin ecosystem (some code codemods and refactoring tools hook into ESLint), evaluate carefully whether the gains outweigh the migration cost. For teams starting new projects or with standard ESLint setups, Biome is worth adopting from day one.


Performance Comparison

The speed difference between ESLint and Biome is significant enough to change how you integrate linting into your workflow:

# Real benchmark: 500 TypeScript files

# ESLint (JavaScript):
# Cold lint: 8.2 seconds
# With cache: 2.1 seconds

# Biome (Rust):
# Cold lint: 0.3 seconds  — 27x faster cold
# Format: 0.1 seconds
# Always cold (no cache needed — it is fast enough)

The practical impact: on a pre-commit hook, ESLint with cache takes 2-3 seconds. Biome takes under a second. This is the difference between a hook that developers work around and one they keep enabled. Fast tooling gets used consistently.


Biome in Monorepos

Biome works particularly well in monorepos because it supports per-package configuration through biome.json files at different directory levels. The root biome.json defines the baseline, and individual packages can extend or override settings.

// packages/ui/biome.json — override root config for this package
{
  "extends": ["../../biome.json"],
  "linter": {
    "rules": {
      "a11y": {
        "useAltText": "error"  // Stricter accessibility rules for UI package
      }
    }
  }
}

For Turborepo monorepos, add a biome task to turbo.json:

// turbo.json
{
  "tasks": {
    "lint": {
      "inputs": ["src/**/*.{ts,tsx}", "biome.json"]
    }
  }
}

Biome's fast execution means it does not need Turborepo's cache as much as ESLint does, but caching still helps for very large monorepos with many packages.


Incremental Migration Strategy

If your codebase is large and you cannot fix all Biome warnings in one session, use Biome's suppression comments to temporarily ignore issues while you migrate incrementally:

// Suppress a specific rule for one line
// biome-ignore lint/suspicious/noConsoleLog: debug in progress
console.log('investigating issue #1234');

// Suppress all rules for a block
// biome-ignore lint: legacy code, refactor pending
const x = eval(userInput);

You can also use the overrides section in biome.json to apply different rules to legacy directories:

// biome.json — relax rules for legacy code
{
  "overrides": [
    {
      "include": ["src/legacy/**"],
      "linter": {
        "rules": {
          "suspicious": {
            "noConsoleLog": "off"
          }
        }
      }
    }
  ]
}

This lets you run biome check . in CI without being blocked by pre-existing issues in legacy code, while still enforcing Biome fully on new code. Over time, clean up the legacy directory and remove the override.


Summary

The ESLint to Biome migration follows a predictable path: install, auto-migrate, handle the gaps, update CI. For most React + TypeScript projects with standard ESLint configs, the migration takes under an hour and the ongoing developer experience improvement is substantial. The 25x speed improvement is real and noticeable in daily use.

The one genuine limitation remains the plugin ecosystem. If your team has custom ESLint rules or relies heavily on plugins that have no Biome equivalent, plan to run both tools together rather than doing a full replacement. The two tools co-exist cleanly — Biome handles the bulk of the work at Rust speed, and a minimal ESLint handles the edge cases.


Further Reading

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.