skip to content

Biome — One toolchain for lint, format, and import sorting

Rust-based all-in-one linter and formatter for JS/TS/JSON/CSS/GraphQL. Single binary, single config, 10-100x faster than ESLint+Prettier.

14 min read 37 snippets deep dive

Biome — One toolchain for lint, format, and import sorting#

What it is#

Biome is a Rust-based toolchain that combines linting, formatting, and import sorting for JavaScript, TypeScript, JSX, TSX, JSON, CSS, and GraphQL into one binary with one config file (biome.json). It is a drop-in replacement for the ESLint + Prettier pair, runs 10–100× faster on cold runs (and uses smart multi-core fan-out), and has zero plugin runtime — every rule is compiled into the binary. The trade-off versus ESLint is plugin maturity: there is no JS plugin ecosystem yet (a WASM plugin system shipped in v2 is still early). For most application repos that don’t need a niche ESLint rule, Biome is a net win in speed, footprint, and ergonomics.

Install#

Biome ships as a single Rust binary distributed via npm. Pin the exact version with --save-exact — patch releases occasionally tweak formatting output, and a pinned version prevents “the linter changed my files in CI” drift.

# npm — recommended, pin exact version
npm install --save-dev --save-exact @biomejs/biome

# pnpm
pnpm add --save-dev --save-exact @biomejs/biome

# yarn
yarn add --dev --exact @biomejs/biome

# bun
bun add --dev --exact @biomejs/biome

# Standalone binary (no Node.js required)
curl -fsSL https://github.com/biomejs/biome/releases/latest/download/biome-linux-x64 -o biome
chmod +x biome

Output:

added 1 package in 1s

1 package is looking for funding
  run `npm fund` for details

Initialize a config in the repo root:

npx @biomejs/biome init

Output:

Welcome to Biome! Let's get you started...

Files created:
  - biome.json

Next steps:
  1. Setup an editor extension: https://biomejs.dev/guides/integrate-in-editor/
  2. Try running biome on your code: npx biome check --write .

The default biome.json enables the recommended rule set, the formatter, and import sorting:

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

Syntax#

Biome uses a verb-first CLI: every operation is a subcommand of the biome binary. The most-used verb is check, which runs lint, format, and import sort in one pass.

biome <command> [options] [paths...]

Output: (none — exits 0 on success)

Essential commands#

CommandWhat it does
biome check <path>Lint + format + import sort (read-only by default)
biome check --write <path>Same, but apply all safe fixes
biome check --write --unsafe <path>Apply unsafe fixes too (review the diff)
biome format <path>Format only (no lint)
biome format --write <path>Format and overwrite files
biome lint <path>Lint only (no format)
biome lint --write <path>Lint and apply safe autofixes
biome ci <path>Like check but optimised for CI: parallel + no writes + machine-readable summary
biome initGenerate a biome.json
biome migrate eslintConvert .eslintrc.* rules to Biome
biome migrate prettierConvert .prettierrc options to Biome
biome explain <ruleName>Print the docs for a rule

check — the everyday command#

biome check is the single command you’ll run 95% of the time. It runs the formatter, the linter, and the import-sort assist as one parallel pipeline and is the recommended replacement for eslint . && prettier --check .. Add --write to apply fixes; in CI use biome ci instead, which is faster and prints a machine-friendly diagnostic summary.

# Check the whole repo (read-only)
npx biome check .

# Check + fix safe issues
npx biome check --write .

# Check + fix safe and unsafe issues (review the diff!)
npx biome check --write --unsafe .

# Check a single directory
npx biome check src/

# Check a glob of files
npx biome check "src/**/*.{ts,tsx}"

Output (clean):

Checked 42 files in 38ms. No fixes applied.

Output (with issues):

src/app.ts:14:3 lint/suspicious/noConsoleLog ━━━━━━━━━━━━

  × Don't use console.log

    12 │ export async function main() {
    13 │   const config = loadConfig();
  > 14 │   console.log(config);
       │   ^^^^^^^^^^^^^^^^^^^
    15 │ }

  i Using console.log in production code is generally discouraged.

  i Safe fix: Remove console.log.

    12 12 │   export async function main() {
    13 13 │     const config = loadConfig();
    14    │ -   console.log(config);
    15 14 │   }


src/utils.ts format ━━━━━━━━━━━━

  i Formatter would have printed the following content:

      1   │ - export   function add(a:number, b:number){return a+b}
      1   │ + export function add(a: number, b: number) {
      2   │ +     return a + b;
      3   │ + }

Checked 42 files in 38ms. Found 2 errors.

biome.json — the single config#

Biome reads exactly one config file (biome.json or biome.jsonc) from the project root. Every option for the formatter, linter, import sorter, and per-language overrides lives in this one file. There is no separate .eslintrc, .prettierrc, .editorconfig-equivalent — and Biome does not merge with parent-directory configs by default (use extends for that).

{
  "$schema": "https://biomejs.dev/schemas/2.1.0/schema.json",

  // Use .gitignore to skip files Git ignores
  "vcs": {
    "enabled": true,
    "clientKind": "git",
    "useIgnoreFile": true,
    "defaultBranch": "main"
  },

  // Files Biome touches
  "files": {
    "includes": ["src/**", "test/**", "*.{js,ts,jsx,tsx,json}"],
    "ignoreUnknown": true
  },

  "formatter": {
    "enabled": true,
    "indentStyle": "space",   // "tab" | "space"
    "indentWidth": 2,
    "lineWidth": 100,
    "lineEnding": "lf"        // "lf" | "crlf" | "cr"
  },

  "javascript": {
    "formatter": {
      "quoteStyle": "double",       // "double" | "single"
      "jsxQuoteStyle": "double",
      "trailingCommas": "all",      // "all" | "es5" | "none"
      "semicolons": "always",       // "always" | "asNeeded"
      "arrowParentheses": "always", // "always" | "asNeeded"
      "bracketSpacing": true,
      "bracketSameLine": false
    }
  },

  "json": {
    "formatter": { "trailingCommas": "none" },
    "parser": { "allowComments": true, "allowTrailingCommas": true }
  },

  "css": {
    "formatter": { "quoteStyle": "double" }
  },

  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true,
      "suspicious": {
        "noConsoleLog": "warn",
        "noExplicitAny": "error"
      },
      "complexity": {
        "noForEach": "off"
      },
      "style": {
        "useImportType": "error",
        "useExportType": "error"
      }
    }
  },

  "assist": {
    "actions": {
      "source": {
        "organizeImports": "on"
      }
    }
  }
}

[!TIP] Use the $schema line — every modern editor will autocomplete and validate the file. The schema URL must match your installed Biome version; the biome migrate command updates it automatically when you upgrade.

Per-file overrides#

Use the top-level overrides array to apply different rules or formatter options to a glob of files. The most common case is relaxing rules for tests, scripts, or generated code.

{
  "overrides": [
    {
      "includes": ["**/*.test.ts", "**/*.spec.ts"],
      "linter": {
        "rules": {
          "suspicious": {
            "noExplicitAny": "off",
            "noConsoleLog": "off"
          }
        }
      }
    },
    {
      "includes": ["scripts/**/*.mjs"],
      "javascript": {
        "formatter": { "quoteStyle": "single" }
      }
    },
    {
      "includes": ["**/*.generated.ts"],
      "linter": { "enabled": false },
      "formatter": { "enabled": false }
    }
  ]
}

Rules and severity#

Biome ships ~280 rules organised into groups: a11y, complexity, correctness, nursery (preview), performance, security, style, and suspicious. Each rule is one of three severities: "off", "warn", or "error". The "recommended": true flag turns on a curated subset (about half the catalogue) at appropriate severities.

{
  "linter": {
    "rules": {
      // Start from the recommended set, then override
      "recommended": true,

      // Promote a warning to an error
      "suspicious": {
        "noDoubleEquals": "error"
      },

      // Demote an error to a warning
      "correctness": {
        "noUnusedVariables": "warn"
      },

      // Disable a rule completely
      "style": {
        "useTemplate": "off"
      },

      // Rule with options — use { level, options }
      "complexity": {
        "noExcessiveCognitiveComplexity": {
          "level": "warn",
          "options": { "maxAllowedComplexity": 15 }
        }
      }
    }
  }
}

List or explain rules from the CLI:

# Show every rule and its current state
npx biome rage --formatter

# Print docs + examples for a single rule
npx biome explain noConsoleLog

Output:

# noConsoleLog
Disallow the use of `console.log`

## Examples

### Invalid

console.log("here");

### Valid

console.info("here");
console.warn("here");

For diagnostic logging, use `console.info`, `console.warn`, or `console.error`.

Suppressing diagnostics inline#

Biome respects three suppression comment forms inside source files. Use them sparingly — a suppression comment without an explanation is a smell.

// Suppress the next line, single rule
// biome-ignore lint/suspicious/noExplicitAny: API returns an opaque blob
function loadConfig(): any { /* ... */ }

// Suppress the next line, all rules
// biome-ignore-all
const intentionallyBad = 1 == 1;

// Suppress a whole range
// biome-ignore-start lint/style/useImportType: legacy file
import { Foo } from "./foo";
import { Bar } from "./bar";
// biome-ignore-end lint/style/useImportType

[!NOTE] Every biome-ignore comment requires an : explanation after the rule name. Biome will lint your suppressions and warn if they’re missing or redundant — a feature ESLint and Prettier lack.

Import sorting#

Biome’s import sorter is an assist action (not a lint rule) — it’s grouped and stable, doesn’t error on misordered imports, and rewrites them on check --write. Groups (in order): bun: protocol, node: protocol, package imports, then path imports.

// Before
import { z } from "zod";
import path from "node:path";
import { Foo } from "./local";
import fs from "fs";
import { x } from "../parent";

// After `biome check --write`
import fs from "node:fs";
import path from "node:path";

import { z } from "zod";

import { x } from "../parent";

import { Foo } from "./local";

Configure it under assist:

{
  "assist": {
    "actions": {
      "source": {
        "organizeImports": "on",
        // Group + sort by custom regex patterns
        "useSortedKeys": "on"
      }
    }
  }
}

Editor integration#

The official VS Code extension is biomejs.biome. Once installed, point the editor at Biome for the relevant languages so that formatOnSave runs biome format instead of Prettier.

// .vscode/settings.json
{
  "editor.defaultFormatter": "biomejs.biome",
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "quickfix.biome": "explicit",
    "source.organizeImports.biome": "explicit"
  },
  "[javascript]":      { "editor.defaultFormatter": "biomejs.biome" },
  "[typescript]":      { "editor.defaultFormatter": "biomejs.biome" },
  "[typescriptreact]": { "editor.defaultFormatter": "biomejs.biome" },
  "[json]":            { "editor.defaultFormatter": "biomejs.biome" },
  "[css]":             { "editor.defaultFormatter": "biomejs.biome" }
}

There are also official plugins for IntelliJ/WebStorm, Zed, Neovim (via nvim-lspconfig), and Helix.

package.json scripts#

A canonical script block exposes the four most-useful commands. The lint and format scripts split work for editor-keybinding-style use; check is the workhorse you’ll add to lint-staged and CI.

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

Pre-commit hook with lint-staged#

npm install -D husky lint-staged
npx husky init

Output: (none — exits 0 on success)

{
  "lint-staged": {
    "*.{js,jsx,ts,tsx,json,css}": [
      "biome check --write --no-errors-on-unmatched"
    ]
  }
}
# .husky/pre-commit
npx lint-staged

Output: (none — exits 0 on success)

The --no-errors-on-unmatched flag stops Biome from failing when lint-staged passes a glob with no matching files (e.g. a commit that only touches .md).

CI usage (GitHub Actions)#

biome ci is the dedicated CI command — it’s check plus parallel mode, no writes, and JSON-friendly diagnostic output via reporters.

name: CI
on: [push, pull_request]

jobs:
  biome:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: biomejs/setup-biome@v2
        with:
          version: 2.1.0
      - run: biome ci . --reporter=github

The --reporter=github flag prints diagnostics in GitHub Actions’ annotation format, so errors show inline on the PR diff. Other reporters: --reporter=summary, --reporter=json, --reporter=junit, --reporter=gitlab.

Migrating from ESLint + Prettier#

Biome ships built-in migration commands that read your existing .eslintrc.*, .prettierrc, .editorconfig, and package.json and write the equivalent settings into biome.json. The migration covers ~95% of typical configs; you’ll need to hand-tweak any custom plugin rules that don’t have a Biome equivalent.

# 1. Install Biome
npm install -D --save-exact @biomejs/biome

# 2. Init Biome (creates biome.json with defaults)
npx biome init

# 3. Pull in your existing ESLint config (rules + severities)
npx biome migrate eslint --write

# 4. Pull in your existing Prettier config (formatter options)
npx biome migrate prettier --write

# 5. Run on the codebase, fixing what's safe
npx biome check --write .

# 6. Optional: review unsafe fixes
npx biome check --write --unsafe .

Output (after migrate eslint):

Migrating ESLint configuration to Biome
  Found .eslintrc.json
  Mapped 23 rules
  Skipped 4 rules with no Biome equivalent:
    - import/order (use Biome's import sorting in `assist.actions.source.organizeImports`)
    - jsx-a11y/click-events-have-key-events
    - react/prop-types
    - simple-import-sort/imports (use Biome's import sorting)
  Wrote biome.json

Then remove the old packages and configs from package.json:

npm uninstall eslint @eslint/js typescript-eslint \
              prettier eslint-config-prettier \
              eslint-plugin-import eslint-plugin-react-hooks
rm .eslintrc.* .prettierrc* .prettierignore

Output: (none — exits 0 on success)

Comparison with ESLint + Prettier#

AspectESLint + PrettierBiome
LanguageJS (Node)Rust (single binary)
Configs2-3 files (.eslintrc, .prettierrc, .prettierignore)1 file (biome.json)
Cold-run speed (10k LOC)~3-8 s~30-100 ms (10-100×)
Hot/incrementalsecondssub-100 ms
Plugin ecosystemHuge (1000+ plugins)Small (WASM plugins in v2, early)
Built-in TS supportNeeds typescript-eslint parser/pluginNative, no separate parser
Built-in JSX supportNeeds config + pluginNative
Built-in JSON / CSS / GraphQLNeeds pluginsNative
Import sortingPlugin (eslint-plugin-import)Built-in assist
Editor LSPSlow, JS-basedFast, native
Suppress-comment lintNoYes (requires explanation)
Custom rulesJS plugin APIGritQL queries (preview)

[!NOTE] The pragmatic decision rule: if your repo doesn’t rely on a niche ESLint plugin you can’t live without (e.g. a domain-specific accessibility plugin or a custom monorepo path-checker), Biome is the better default for new projects in 2026. For very mature codebases with deep ESLint plugin investment, the migration cost can outweigh the speed win.

Common pitfalls#

  1. Forgetting --save-exact — formatter output can change between minor versions, leading to CI churn. Always pin Biome with --save-exact and bump deliberately.
  2. Mixing Biome with Prettier formatting on the same files — pick one. If you keep Prettier for .md files only, scope each tool with includes to non-overlapping globs.
  3. biome check modifies files — it doesn’t, unless you pass --write. The default is read-only; running it in CI without --write is correct.
  4. --unsafe fixes break code — that’s why they’re unsafe. Always review the diff before committing. Common offenders: rewriting == to === when the loose equality was intentional.
  5. No support for .eslintrc overrides at runtime — Biome reads biome.json only. Migrate once with biome migrate eslint; don’t try to keep both configs in sync.
  6. VS Code formatter mismatch — installing the Biome extension does not change the default formatter. You must explicitly set editor.defaultFormatter per language (see snippet above) or saving will still run Prettier.
  7. Ignoring node_modules slowly — by default Biome respects .gitignore only if vcs.useIgnoreFile: true is set. Without that, it walks every file. Turn the VCS option on for large repos.
  8. CI not seeing errors inline — pass --reporter=github (GitHub Actions) or --reporter=gitlab (GitLab CI) so annotations show on the PR/MR diff instead of buried in logs.

Real-world recipes#

Migrating a Next.js app from ESLint + Prettier to Biome#

A typical Next.js repo with eslint-config-next + Prettier + eslint-plugin-import migrates in five steps.

# 1. Install Biome (pinned)
npm install -D --save-exact @biomejs/biome

# 2. Init + import old configs
npx biome init
npx biome migrate eslint --write
npx biome migrate prettier --write

# 3. Apply formatting + import-sort to the whole tree
npx biome check --write .

# 4. Replace the old npm scripts
#    "lint": "next lint"          ->  "lint": "biome check ."
#    "format": "prettier --write" ->  "format": "biome format --write ."
#    "check": "biome check ."

# 5. Remove dead deps + configs
npm uninstall eslint eslint-config-next prettier \
              eslint-config-prettier eslint-plugin-import
rm .eslintrc.json .prettierrc .prettierignore

Output:

Migrating ESLint configuration to Biome
  Found .eslintrc.json (extends: next, prettier)
  Mapped 18 rules
  Skipped 6 rules with no direct equivalent (most have Biome equivalents via different rule names — review biome.json)
Checked 412 files in 287ms. Applied 31 fixes.

Pre-commit + CI matrix#

A repo that wants Biome to gate both commits and PRs.

# Local: lint-staged + husky
npm install -D husky lint-staged
npx husky init

Output: (none — exits 0 on success)

{
  "scripts": { "prepare": "husky" },
  "lint-staged": {
    "*.{js,jsx,ts,tsx,json,css}": [
      "biome check --write --no-errors-on-unmatched --files-ignore-unknown=true"
    ]
  }
}
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: biomejs/setup-biome@v2
        with: { version: 2.1.0 }
      - run: biome ci . --reporter=github

  build:
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm ci
      - run: npm run build

Differential lint — only changed files in a PR#

For monorepos where checking the whole tree on every commit is overkill, lint only files changed against main.

# Get the changed file list
CHANGED=$(git diff --name-only --diff-filter=ACMR origin/main...HEAD | grep -E '\.(js|jsx|ts|tsx|json|css)$')

# Hand them to Biome (xargs splits long lists across multiple invocations)
echo "$CHANGED" | xargs -r npx biome check --no-errors-on-unmatched

Output:

Checked 8 files in 21ms. No fixes applied.

Multi-package monorepo — one config, package-local overrides#

Put a single biome.json at the repo root and use overrides.includes to scope per-package rules.

// biome.json (repo root)
{
  "$schema": "https://biomejs.dev/schemas/2.1.0/schema.json",
  "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
  "linter": { "rules": { "recommended": true } },
  "overrides": [
    {
      "includes": ["packages/api/**"],
      "javascript": { "formatter": { "quoteStyle": "single" } },
      "linter": {
        "rules": { "suspicious": { "noConsoleLog": "error" } }
      }
    },
    {
      "includes": ["packages/web/**"],
      "linter": {
        "rules": { "a11y": { "recommended": true } }
      }
    },
    {
      "includes": ["packages/*/test/**"],
      "linter": {
        "rules": { "suspicious": { "noExplicitAny": "off" } }
      }
    }
  ]
}

Then run from the root:

npx biome check packages/

Output:

Checked 1284 files in 412ms. No fixes applied.

Editor save-on-write with safe-only fixes#

A repo that wants format-on-save plus safe lint fixes (e.g. useImportType) but never unsafe ones.

// .vscode/settings.json
{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "biomejs.biome",
  "editor.codeActionsOnSave": {
    "source.fixAll.biome": "explicit",
    "source.organizeImports.biome": "explicit"
  },
  "biome.lspBin": "./node_modules/@biomejs/biome/bin/biome"
}

The biome.lspBin line forces VS Code to use the project’s pinned Biome binary instead of the extension’s bundled one — important for reproducibility on shared teams.