tooling

Migrate from ESLint + Prettier to Biome (2026 Guide)

June 19, 2026

Migrate from ESLint + Prettier to Biome (2026 Guide)

Biome is a single Rust-based tool that replaces both ESLint and Prettier. Install it, run biome migrate eslint and biome migrate prettier to convert your existing configs into one biome.json, then run biome check --write to lint and format your project in a single pass.1

TL;DR

You will take a small JavaScript project that uses ESLint and Prettier and move it to Biome 2.5.0 in about ten minutes. We install Biome, generate a config with biome init, migrate the existing ESLint and Prettier settings automatically, run the combined lint-and-format checker, wire up npm scripts, and add a CI gate. Every command and output below was run on Node.js 22.22.3. No screenshots — this is pure terminal work.

What you'll learn

  • Why teams replace ESLint + Prettier with Biome, and what the trade-offs are
  • How to install and pin Biome 2.5.0 and scaffold a biome.json
  • How to migrate an existing ESLint config with biome migrate eslint
  • How to migrate a Prettier config and .prettierignore with biome migrate prettier
  • How to lint and format in one pass with biome check, including safe vs. unsafe fixes
  • How to add npm scripts and gate CI with biome ci
  • How to make Biome respect your .gitignore
  • How to troubleshoot the five things that actually trip people up

Prerequisites

  • Node.js 20 or newer (tested on 22.22.3). Biome ships as a prebuilt native binary through npm, so there is nothing to compile.1
  • An existing project that already uses ESLint (flat or legacy .eslintrc*) and Prettier. If you don't have one, the next step creates a minimal one you can copy-paste.
  • A terminal. That's it — Biome has no peer dependencies and does not need the typescript package to lint TypeScript.2

Why Biome instead of ESLint + Prettier

ESLint and Prettier are two tools, two config files, two dependency trees, and two passes over your code. Biome collapses them into one binary with one config. Biome's own published figures put it at 97% compatibility with Prettier's formatting and roughly 35x faster than Prettier on its benchmark of 171,127 lines across 2,104 files.3 Because Biome 2.0 added a lightweight type-inference engine, it can run type-aware lint rules without invoking the TypeScript compiler or installing the typescript package, which is what keeps it fast.2

The honest trade-off: that remaining 3% of Prettier differences can matter for some teams, and Biome's rule set — while past 500 lint rules as of v2.5 — does not cover every ESLint plugin you may depend on.4 The migration commands below tell you exactly what percentage of your rules Biome can reproduce, so you can make that call with data instead of guesswork.

Step 1: Start from a real ESLint + Prettier project

Create a tiny project so every output in this guide is reproducible. The source file has deliberate lint problems (loose equality, an unused variable, a stray console.log, a let that should be const) and bad formatting.

mkdir biome-demo && cd biome-demo
npm init -y

Add a legacy ESLint config (.eslintrc.json):

{
  "root": true,
  "rules": {
    "no-unused-vars": "error",
    "no-console": "warn",
    "eqeqeq": "error",
    "prefer-const": "error"
  }
}

Add a Prettier config (.prettierrc):

{
  "semi": false,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "all",
  "printWidth": 100
}

Add a .prettierignore:

dist
coverage

And a messy source file at src/total.js:

let   label = "cart"
const taxRate = 0.2
const unusedTotal = 0

function total(items, applyTax){
    let sum = 0
    for (const i of items){
        sum = sum + i.price
    }
    if (applyTax == true){
        console.log("applying tax")
        sum = sum * (1 + taxRate)
    }
    return sum
}

export {total, label}

Step 2: Install and pin Biome

Install Biome as a dev dependency and pin the exact version so CI and every teammate run the identical binary. Pinning matters: Biome ships new and promoted rules in minor releases, so an unpinned install can change which diagnostics fire.4

npm install -D @biomejs/biome@2.5.0
npx biome --version
Version: 2.5.0

Now scaffold a config:

npx biome init

This writes a starter biome.json. The defaults enable the formatter (tab indent), the linter with the recommended preset, and import sorting — and include a vcs block (off by default) that we turn on in Step 8:

{
  "$schema": "https://biomejs.dev/schemas/2.5.0/schema.json",
  "vcs": {
    "enabled": false,
    "clientKind": "git",
    "useIgnoreFile": false
  },
  "files": {
    "ignoreUnknown": false
  },
  "formatter": {
    "enabled": true,
    "indentStyle": "tab"
  },
  "linter": {
    "enabled": true,
    "rules": {
      "preset": "recommended"
    }
  },
  "javascript": {
    "formatter": {
      "quoteStyle": "double"
    }
  },
  "assist": {
    "enabled": true,
    "actions": {
      "source": {
        "organizeImports": "on"
      }
    }
  }
}

Note "preset": "recommended". As of v2.5, the older "recommended": true key is deprecated in favor of linter.rules.preset; both still work, but biome init and biome migrate now emit preset, and running biome migrate --write rewrites the old key for you.4 Many older tutorials still show recommended: true — that is the deprecated form.

Step 3: Migrate your ESLint config

This is the headline feature. Biome reads your existing ESLint config and rewrites biome.json with the equivalent Biome rules — and tells you the coverage:

npx biome migrate eslint --write
i 4 ESLint rules found
  - 4 have been migrated to Biome's rules
  - 100% (4) of your ESLint rules are fully covered by Biome
    - 100% (4) via direct migration to Biome rules
- biome.json: configuration successfully migrated.

Open biome.json and you'll see the linter block now mirrors your four ESLint rules exactly:

"linter": {
  "enabled": true,
  "rules": {
    "preset": "none",
    "correctness": { "noUnusedVariables": "error" },
    "style": { "useConst": "error" },
    "suspicious": { "noConsole": "warn", "noDoubleEquals": "error" }
  }
}

Two things to understand here. First, the mapping: no-unused-vars became noUnusedVariables, prefer-const became useConst, eqeqeq became noDoubleEquals, and no-console kept its warn severity. Second — and this surprises people — the migration set "preset": "none". It deliberately reproduces only the rules you had in ESLint, rather than layering Biome's recommended set on top, so your linting behavior doesn't silently change. If you want Biome's recommended rules as well, change none back to recommended after migrating.

Step 4: Migrate your Prettier config

Same idea for formatting. Biome maps .prettierrc and .prettierignore into the same biome.json:

npx biome migrate prettier --write
.prettierignore has been successfully migrated.
.prettierrc has been successfully migrated.
- biome.json: configuration successfully migrated.

Your Prettier settings land in the formatter and javascript.formatter blocks, and .prettierignore becomes negated globs in formatter.includes. Here are the Prettier-derived fields (Biome also writes its own formatter defaults, such as bracketSpacing and attributePosition, which are omitted here for brevity):

"formatter": {
  "enabled": true,
  "indentStyle": "space",
  "indentWidth": 2,
  "lineWidth": 100,
  "lineEnding": "lf",
  "includes": ["**", "!**/dist", "!**/coverage"]
},
"javascript": {
  "formatter": {
    "quoteStyle": "single",
    "semicolons": "asNeeded",
    "trailingCommas": "all"
  }
}

That maps cleanly: singleQuote: truequoteStyle: "single", semi: falsesemicolons: "asNeeded", trailingComma: "all"trailingCommas: "all", tabWidth: 2indentWidth: 2, printWidth: 100lineWidth: 100. Your one config file now carries everything ESLint and Prettier used to.

Step 5: Run your first check, then auto-fix

biome check runs the formatter, the linter, and import sorting together. Its default output prints a full code frame and suggested fix for each problem; add --reporter=concise for a one-line-per-issue summary. Run it against the messy file:

npx biome check --reporter=concise ./src
! src/total.js:11:9: lint/suspicious/noConsole: Don't use console.
× src/total.js:10:18: lint/suspicious/noDoubleEquals: Using == may be unsafe if you are relying on type coercion.
× src/total.js:17:1: assist/source/organizeImports: Sort the exported names.
× src/total.js:1:1: lint/style/useConst: This let declares a variable that is only assigned once.
× src/total.js:3:7: lint/correctness/noUnusedVariables: This variable unusedTotal is unused.
× src/total.js: format: Formatter would have printed the following content:
Checked 1 file in 2ms. No fixes applied.
Found 5 errors.
Found 1 warning.

The command exits non-zero (1) because problems were found. Every line marked × is an error — that's five, counting the formatting difference — while noConsole, marked !, is a warning. Now apply the safe fixes — the ones Biome can make without any risk of changing behavior:

npx biome check --write ./src

After this, src/total.js is reformatted, let label became const label (it's only assigned once), and the export names are sorted. Note that let sum stayed a let because it is reassigned in the loop — Biome's useConst is precise, not blanket:

const label = 'cart'
const taxRate = 0.2
const unusedTotal = 0

function total(items, applyTax) {
  let sum = 0
  for (const i of items) {
    sum = sum + i.price
  }
  if (applyTax == true) {
    console.log('applying tax')
    sum = sum * (1 + taxRate)
  }
  return sum
}

export { label, total }

Two problems remain on purpose: the == and the unused unusedTotal. Biome classifies their fixes as unsafe, because removing a console.log, swapping == for ===, or renaming an unused variable can change runtime behavior. Apply them explicitly with --unsafe:

npx biome check --write --unsafe ./src
Checked 1 file in 1.9ms. Fixed 1 file.

Now applyTax == true becomes applyTax === true, the console.log line is removed, and the unused unusedTotal is renamed to _unusedTotal (Biome's unsafe fix prefixes rather than deletes, so you don't lose the declaration silently). The command exits 0. The split between safe and unsafe fixes is the single most useful habit to learn: run --write in pre-commit hooks, and reserve --write --unsafe for deliberate, reviewed cleanups.

Step 6: Add npm scripts and your editor

Wire Biome into package.json so the whole team runs the same commands:

"scripts": {
  "lint": "biome lint ./src",
  "format": "biome format --write ./src",
  "check": "biome check --write ./src",
  "ci": "biome ci ./src"
}

biome lint runs only the linter, biome format --write runs only the formatter, and biome check does both plus import sorting. For live feedback while you type, install the official Biome editor extension (VS Code, Zed, or JetBrains) and set Biome as your default formatter; the extension uses the same biome.json, so editor and CLI never disagree.1

Step 7: Gate CI with biome ci

biome ci is a dedicated, never-writes command for pipelines: it checks formatting, lint, and import order, and exits non-zero if anything is off. On a clean tree it passes:

npx biome ci ./src
Checked 1 file in 1.2ms. No fixes applied.

Exit code 0. Introduce one badly formatted file and it fails the build:

Checked 2 files in 1.2ms. No fixes applied.
Found 2 errors.

Exit code 1. Drop that into a GitHub Actions step and your pipeline blocks unformatted or lint-failing code:

- name: Biome
  run: npx biome ci ./src

If you already run keyless cloud deploys from Actions, this slots in next to them — see our GitHub Actions OIDC keyless deploys tutorial for the surrounding workflow. For large monorepos, the new --reporter=concise output keeps logs short.4

Step 8: Make Biome respect your .gitignore

"vcs": {
  "enabled": true,
  "clientKind": "git",
  "useIgnoreFile": true
}

With that set and a .gitignore that lists src/generated.js, a run only inspects your real source:

npx biome check ./src
Checked 1 file in 1.1ms. No fixes applied.

Only one file was checked — the generated file was skipped. As of v2.5, Biome also honors .git/info/exclude the same way it honors .gitignore.4

Verification

You're done when a clean checkout passes both commands with exit code 0:

npx biome check ./src   # exit 0, "No fixes applied."
npx biome ci ./src      # exit 0
echo $?                 # prints 0

At that point you can delete .eslintrc.json, .prettierrc, .prettierignore, and uninstall eslint, prettier, and their plugins. One binary, one config, one pass.

Troubleshooting

migrate eslint errors with Cannot find package '@eslint/js'. If your ESLint config uses extends: ["eslint:recommended"] (or any shared/plugin config), Biome loads it through Node to resolve those references, which fails if you've already removed ESLint. The fix is ordering: migrate before you uninstall ESLint. With ESLint still installed so it can expand eslint:recommended, the same command finds about 67 ESLint rules, migrates 61 of them, and reports roughly 92% coverage. Migrate first, verify, then remove ESLint.

An unused import isn't flagged after migration. ESLint's no-unused-vars maps to Biome's noUnusedVariables, which covers variables — not imports. Biome splits unused imports into a separate rule, noUnusedImports. If you relied on no-unused-vars to catch dead imports, add "correctness": { "noUnusedImports": "error" } to your linter rules after migrating.

You lost Biome's recommended rules. Expected: migrate eslint sets "preset": "none" so it reproduces only your ESLint rules. To get Biome's curated recommended set as well, change it to "preset": "recommended" and re-run biome check to see the new diagnostics.

A teammate's config uses "recommended": true. That key is deprecated as of v2.5 in favor of preset. It still works today, but run biome migrate --write to update it before it's removed in a future major.4

--write didn't fix everything. That's by design. Biome only auto-applies safe fixes with --write; behavior-changing fixes (loose equality, dead console calls, unused bindings) require --write --unsafe. Review those diffs rather than running --unsafe blindly in a hook.

Next steps and further reading

Biome pairs naturally with the rest of a modern toolchain: if you're already chasing build speed, the native TypeScript compiler (tsgo) is the companion move for type-checking, and a refresher on modern JavaScript patterns makes Biome's rule output easier to read. From here, explore Biome's GritQL plugin system to write project-specific rules, enable type-aware lint rules through domains, and turn on --watch for live diagnostics while you code.4

Footnotes

  1. Biome — Getting Started and homepage. https://biomejs.dev/guides/getting-started/ 2 3

  2. Biome — "Roadmap 2025 and Biome 2.0" (type-aware linting without the TypeScript compiler; plugins; domains; multi-file analysis). https://biomejs.dev/blog/roadmap-2025/ 2

  3. Biome — homepage formatter claims: 97% Prettier compatibility and ~35x faster than Prettier (benchmark: 171,127 lines across 2,104 files, Intel Core i7 1270P). https://biomejs.dev/

  4. Biome — "Biome v2.5—500 Lint Rules, Plugin Code Fix, and Cross-File Linting" (Jun 5, 2026): 500+ rules, recommended deprecated for preset, --watch, concise reporter, .git/info/exclude support, .svg support. https://biomejs.dev/blog/biome-v2-5/ 2 3 4 5 6 7