Biome, a formatter and linter in one tool
Biome is a Rust binary that formats code and analyses it statically, taking the place of the ESLint plus Prettier pair in a project. The current version is 2.5.9, released on 17 August 2026, the biomejs/biome repository holds roughly 25.6 thousand stars, and the licence is dual: MIT or Apache 2.0, at the recipient's choice.
What Biome actually does
Three separate functions live inside one executable, and telling them apart saves considerable confusion during configuration.
The first is the formatter. It does exactly what Prettier does: it takes a syntactically valid file and prints it back according to fixed line-breaking rules. The second is the linter, meaning static analysis looking for bugs and unwanted patterns, with its own rule set and its own switch. The third is called assist and covers housekeeping actions that are neither formatting nor error reporting: sorting imports, sorting object keys, removing duplicate CSS classes. In version one, import sorting was a top-level field; in version two it moved into assist.actions.source.organizeImports, and that is the most common reason a copied configuration suddenly stops working.
On top of that sits a language server, started through biome lsp-proxy or an editor extension, plus a background daemon holding the indexed state of the project.
Language coverage is narrower than the slogan about tooling for the whole front end suggests. Full support covers JavaScript at ES2024, TypeScript 5.9, JSX, JSON and JSONC, CSS and GraphQL. Vue, Svelte and Astro files have been handled since version 2.3.0, but are explicitly marked experimental and require enabling through html.experimentalFullSupportEnabled. SCSS, YAML and Markdown are in progress and do not work today. If your repository formats YAML pipeline definitions with Prettier, Biome will not take them over.
What Biome does not do matters just as much. It does not check types, so tsc --noEmit stays in the pipeline. It does not run tests, which is what Vitest is for. It does not build the application, since that is Vite territory. It does not manage monorepo caching, which is handled by Turborepo.
Version, licence and project health
Releases arrive regularly and densely. Versions 2.5.1 through 2.5.9 shipped between 23 June and 17 August 2026, roughly one a week. The last change on the main branch dates from 20 August 2026, the repository is not archived, and it carries 1196 forks and 534 open issues.
The licence comes with a discrepancy that is easy to misread during a dependency audit. The license field in the npm registry for @biomejs/biome reads MIT OR Apache-2.0. The published package contains three licence files: LICENSE-APACHE, LICENSE-MIT and ROME-LICENSE-MIT, the last being the MIT text from 2020 to 2023 inherited from the Rome project that Biome grew out of. Both the Apache and the MIT file sit in the repository root. The GitHub programming interface, however, reports a single licence for that repository, Apache 2.0, because its detector picks one value and cannot express a dual choice. The factual position is that you may pick either of two permissive licences, while any tool harvesting metadata from GitHub will show you only one of them. If your company keeps a dependency licence list, record both.
There is no paid variant. The page labelled Enterprise carries no price list and no plans, only a note that some contributors accept paid commercial work, plus links to a Discord channel and a GitHub issue form. Funding runs through OpenCollective and GitHub Sponsors, and Vercel sponsored the work on the type inference engine. That is a typical open-source arrangement and carries a known risk: development depends on a handful of maintainers and on sponsor money rather than on an enforceable support contract.
Distribution has consequences too. The @biomejs/biome package itself has no dependencies, but declares eight optional packages carrying binaries: Linux, macOS and Windows in x64 and arm64 variants, plus two musl variants for Linux. Installing with the flag that skips optional dependencies will not produce a working program. The engines field requires Node 14.21.3 or newer, which in practice imposes no real limit.
Installation and first run
The order of commands when entering an existing project looks like this.
# pin the exact version rather than a range
npm install --save-dev --save-exact @biomejs/biome
# generate biome.json in the current directory
npx biome init
# check without modifying files
npx biome check
# formatting, fixable rules and assist actions in one pass
npx biome check --write
# the same, including fixes marked unsafe
npx biome check --write --unsafe
# build server mode: writes nothing, returns an exit code
npx biome ci
# only files changed against the main branch
npx biome check --changed --since=main
# documentation for a single rule straight from the terminal
npx biome explain noFloatingPromisesThe --save-exact flag is not overkill. Formatter output gets tuned between releases, and with a version range two people on a team can hold different binaries and reformat each other's files. Given weekly releases, the risk is real.
The biome explain command takes a rule name and prints its group, default severity, fix kind, the version it became available in, and its domain membership. For noFloatingPromises you will see the category lint/nursery/noFloatingPromises, a default severity of info, and a fix marked unsafe. That is a faster way to check a rule's status than hunting through the documentation.
Configuration in biome.json
The biome.json or biome.jsonc file lives at the project root. Below is a configuration tested against version 2.5.9, with no warnings about deprecated fields.
{
"$schema": "https://biomejs.dev/schemas/2.5.9/schema.json",
"root": true,
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true,
"defaultBranch": "main"
},
"files": {
"includes": ["**", "!**/dist", "!**/coverage"],
"maxSize": 1048576
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100,
"lineEnding": "lf"
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"jsxQuoteStyle": "double",
"semicolons": "asNeeded",
"trailingCommas": "all",
"arrowParentheses": "asNeeded"
}
},
"linter": {
"enabled": true,
"rules": {
"preset": "recommended",
"correctness": {
"useExhaustiveDependencies": "error",
"noUnusedImports": "error"
},
"suspicious": {
"noExplicitAny": "warn"
},
"style": {
"useImportType": "error"
}
}
},
"assist": {
"enabled": true,
"actions": {
"source": {
"organizeImports": "on"
}
}
},
"overrides": [
{
"includes": ["**/*.test.ts", "**/*.test.tsx"],
"linter": {
"rules": {
"suspicious": { "noExplicitAny": "off" }
}
}
}
]
}Several details in that file deserve comment. The preset field accepts recommended, all or none and replaced the older recommended: true, which still works in 2.5.9 but emits a deprecation warning and will disappear in the next major release. Patterns in files.includes use an exclamation mark for negation, and a directory is excluded by writing !**/dist rather than !**/dist/**. The latter form was a workaround for a bug in versions before 2.2.0 and today Biome itself flags it through the useBiomeIgnoreFolder rule. Setting vcs.useIgnoreFile makes the tool honour .gitignore, which it does not do by default.
The default indent is a tab and the default quotes are double. If the project previously sat on Prettier with spaces, set indentStyle explicitly or enable useEditorconfig, otherwise the first run will rewrite the entire repository.
In a monorepo each subproject may carry its own configuration file, but it has to be marked as non-root through "root": false. The shorthand "extends": "//" sets the same thing and additionally inherits settings from the root configuration. Without that marker Biome will treat the subdirectory as a separate project.
Domains, or rules switched on by dependencies
Domains are the mechanism that separates Biome from the classic arrangement with a list of plugins. Instead of installing eslint-plugin-react and adding it to the configuration, you enable the react domain and the rules meant for that library become active.
{
"linter": {
"domains": {
"react": "recommended",
"next": "recommended",
"test": "recommended",
"tailwind": "all",
"project": "recommended",
"types": "none"
}
}
}There are fifteen domains: astro, drizzle, react, reactNative, test, solid, next, qwik, svelte, vue, project, tailwind, turborepo, playwright and types. Each takes the value recommended, all or none.
Without an explicit entry, a domain switches itself on when the matching dependency appears in package.json. The thresholds are concrete: react from version 16, next from 14, tailwindcss from 3, turbo from 1, drizzle-orm from 0.9, @playwright/test from 1, while the test domain reacts to jest, mocha, ava or vitest. Domains also introduce global identifiers, so describe and expect in test files are not reported as unknown.
Two domains behave differently from the rest and you need to know that before enabling them. project and types start a scanner that indexes every project file along with node_modules. The project domain holds rules such as noUnresolvedImports, noUndeclaredDependencies, noImportCycles and noPrivateImports, precisely the ones a single file is not enough to check. The types domain additionally starts the type inference engine. Both cost time, and no rule requiring the scanner is ever recommended outside its own domain, which is a deliberate decision by the team in favour of speed.
There is also a naming trap. In several domains, among them tailwind, playwright, astro, drizzle and reactNative, every rule sits in the nursery group. Setting the value to recommended will enable nothing there, because rules from that group are never recommended. You have to use all or list rules one by one. This applies, for instance, to sorting Tailwind CSS classes, often cited as the reason somebody stays with ESLint.
Migrating from ESLint and Prettier step by step
Biome ships commands that import settings from both tools, and that is the most sensible starting point.
# 1. baseline configuration
npx biome init
# 2. import the formatter settings
npx biome migrate prettier --write
# 3. import the lint rules together with a coverage report
npx biome migrate eslint --write
# 4. the same, including rules merely inspired by the original
npx biome migrate eslint --write --include-inspiredThe report from step three is the single most important thing in the whole migration, because it states plainly how much is left on the floor. For a configuration holding six rules it looks like this.
i 6 ESLint rules found
- 1 have been migrated to Biome's rules
- 50% (3) of your ESLint rules are fully covered by Biome
- 16% (1) via direct migration to Biome rules
i Rules that can be migrated to an inspired rule using --include-inspired:
- max-lines
- no-magic-numbers
i Unsupported rules (0 incompatible with formatter, 0 made obsolete by the
formatter, 0 covered by a formatter option, 3 not yet implemented,
0 unknown source):
- consistent-return
- no-warning-comments
- sort-keysThe categories are well thought out. Rules incompatible with the formatter and rules made obsolete by it can be struck off safely, because Biome enforces their effect anyway. Rules covered by a formatter option move into the formatter section. What remains is the "not yet implemented" category, and that is the measure of the migration cost.
A few notes on the command itself. It overwrites the existing configuration and sets preset to none, so after migration you hold exactly the rules you had in ESLint and no others. Loading a flat configuration requires Node, because it genuinely executes eslint.config.js together with its plugins. Configurations written in YAML are not supported. It does carry over .eslintignore. Plugins and shared configurations exporting an object with a cyclic reference can stall the whole process, and the workaround is commenting them out one at a time and repeating the migration.
The rest of the order is simple. Reformat the entire repository with a single biome check --write and land it as a separate commit whose hash you add to .git-blame-ignore-revs, so as not to ruin change history. Update editor extensions, because two formatters running on save produce a result that depends on ordering. Only at the end remove the ESLint and Prettier dependencies, and if some plugin turned out to be irreplaceable, keep ESLint with a configuration narrowed down to that plugin alone.
What you do not get back after migrating
The 2.5.9 configuration schema lists 530 lint rules across eight groups: suspicious 121, correctness 100, style 100, nursery 96, complexity 51, a11y 39, performance 16 and security 7. Subtract the nursery group, which by definition is not recommended and may change without notice, and 434 rules remain ready to use. That is a lot, but the distribution against the ESLint ecosystem is very uneven.
The official rule sources page gives numbers worth seeing before you decide. From core ESLint 106 rules were carried over, from typescript-eslint 44, from eslint-plugin-jsx-a11y 33, from Stylelint 23, from eslint-plugin-unicorn 21, from eslint-plugin-react 14, from eslint-plugin-jest 7, from @next/eslint-plugin-next 5, from eslint-plugin-import 4, from eslint-plugin-barrel-files 3, and two each from eslint-plugin-react-hooks, eslint-plugin-sonarjs and eslint-plugin-unused-imports.
Those numbers read unevenly. Accessibility and the basic React rules are covered decently. By contrast eslint-plugin-import with four equivalents looks thin, although part of its role is taken over by the project domain rules. Most striking is eslint-plugin-react-hooks with two rules, useExhaustiveDependencies and useHookAtTopLevel. Rules tied to the React compiler do exist, but as useReactCompiler inside the nursery group.
Rules requiring type information are a story of their own. Biome's inference engine works without the typescript package, which is a considerable engineering achievement, but the team itself reports that the noFloatingPromises rule catches about 75 percent of the cases typescript-eslint would catch, and notes the measurement is preliminary and taken on a limited sample. Nearly the whole types domain, meaning noMisusedPromises, useAwaitThenable, noBaseToString, noUnsafePlusOperands and the rest, still sits in the nursery group. If your TypeScript project leans on type-aware rules, that is where the migration hurts most.
The rule sources list contains no entries at all for plugins such as eslint-plugin-jsdoc, eslint-plugin-testing-library, eslint-plugin-storybook or eslint-plugin-perfectionist. That does not mean no individual rule with similar behaviour exists, but whole sets have not been ported and nobody has announced plans to port them.
There is also the options layer. The documentation states outright that Biome rules can be poorer in options than the originals. A rule with the same name may therefore exist and still refuse to be tuned the way it was tuned in ESLint. Names differ too, because Biome uses camelCase and often renames a rule to describe its intent better, so searching the documentation by the old name usually fails.
GritQL plugins instead of JavaScript plugins
The most serious architectural limitation follows from the implementation language. Since Biome is a Rust binary, you cannot add a rule written in JavaScript the way you add an ESLint plugin. What you get instead is GritQL, a declarative language for matching patterns in code.
language js
`console.log($msg)` as $call where {
register_diagnostic(
span = $call,
message = "Use console.info instead of console.log.",
severity = "warn",
fix_kind = "safe"
),
$call => `console.info($msg)`
}A file with the .grit extension is attached through the plugins field, optionally narrowed to selected paths with includes. The => operator performs a rewrite, with the caveat that without the --write flag the fix is only suggested, and fixes without an explicit fix_kind are treated as unsafe.
The boundaries of this arrangement are sharp. Beyond GritQL's built-in functions, exactly one function added by Biome is available, register_diagnostic, taking four arguments. The target languages are JavaScript, CSS and JSON. A pattern has no access to type inference results or to the module graph. Rule options cannot be defined either, so every parameterised behaviour has to become a separate file. For company-wide syntactic rules that is enough. For porting a non-trivial ESLint plugin it usually is not.
Biome against the alternatives
| Feature | Biome | ESLint plus Prettier | Oxlint | dprint | Deno |
|---|---|---|---|---|---|
| Scope | formatter and linter | formatter and linter | linter only | formatter only | part of a runtime |
| Implementation | Rust | JavaScript | Rust | Rust | Rust |
| Custom plugins | GritQL | JavaScript | JavaScript | WebAssembly | none |
| Type-aware rules | types domain, mostly nursery | full through typescript-eslint | limited | not applicable | limited |
| Current version | 2.5.9 | 10.8.1 and 3.9.6 | 1.79.0 | 0.56.0 | 2.9.5 |
| Licence | MIT or Apache 2.0 | MIT | MIT | MIT | MIT |
The choice comes down to two questions. If your ESLint configuration rests on plugins outside the ported list or on type-aware rules, stay with ESLint and at most hand formatting over to Biome, since that is the safest part and the one yielding the largest time saving. If the configuration fits within core rules, typescript-eslint and accessibility, one tool instead of two simplifies the pipeline and cuts the checking step in Next.js or any other project down to a fraction of its former time.
A third route is Oxlint, the closest relative in this table: also in Rust, also fast, but deliberately a linter only, handing formatting to a separate oxfmt tool. It also differs in its plugin model, letting you write plugins in JavaScript rather than GritQL, which lowers the barrier to entry. The drawback sits on the other side: rules using type information need a separate add-on there and TypeScript 7.0 or newer, so on an older version that part simply will not run.
Common mistakes
The first is leaving ESLint, Prettier and Biome all enabled without dividing responsibilities. Two formatters running on save produce a result that depends on extension ordering in the editor, and the differences come back in every change request. If you keep both tools, disable everything formatting-related in ESLint.
The second is ignoring the default indent. Biome indents with a tab by default, Prettier with spaces. A first biome check --write on a repository formatted by Prettier will rewrite every file, and the resulting change set will be impossible to review.
The third is exclusion patterns written the old way. The form !**/dist/** dates from before version 2.2.0 and is today flagged by the useBiomeIgnoreFolder rule. The correct form is !**/dist.
The fourth is "recommended": true in the rules section. The field still works, but emits a deprecation warning and will disappear in the next major version. Replace it with "preset": "recommended", or simply run biome migrate --write, which rewrites the configuration for you.
The fifth is enabling the project or types domain without awareness of the cost. Both start a full project scan including node_modules, so the difference in run time is noticeable. A sensible arrangement keeps those domains in the continuous integration pipeline and disables them in the configuration the editor uses.
The sixth is expecting stability from rules in the nursery group. They may be renamed, change behaviour or vanish between minor releases, and releases arrive weekly. If you enable them explicitly, pin an exact Biome version.
The seventh is unmarked nested configurations. A biome.json in a subdirectory without "root": false or "extends": "//" will be treated as a separate project, and rules relying on package.json will reach for the wrong file.
FAQ
Will Biome replace ESLint in every project?
No. It replaces it where the configuration rests on core ESLint rules, on typescript-eslint and on accessibility. If you use plugins absent from the ported sources list, or rules requiring full type information, you will stay with ESLint or maintain both tools side by side.
Is the typescript package needed to lint TypeScript?
Not for linting itself. Biome carries its own inference engine and works without the TypeScript compiler. Type checking is a separate matter, though, and tsc --noEmit still has to sit in the pipeline, because Biome does not replace it.
Can I write a custom rule in JavaScript?
No. The only route is GritQL plugins written in .grit files and attached through the plugins field. They have access to one function added by Biome, register_diagnostic, they support JavaScript, CSS and JSON, they see neither types nor the module graph, and they take no options.
Does Biome format YAML and Markdown?
Not today. Full support covers JavaScript, TypeScript, JSX, JSON and JSONC, CSS and GraphQL. Vue, Svelte and Astro work from version 2.3.0 as an experimental feature requiring explicit enabling. SCSS, YAML and Markdown are in progress.
Is Biome paid?
No. The whole project is open source, under MIT or Apache 2.0 at your choice, with no paid tier and no restrictions on commercial use. The Enterprise page carries no price list, only a note about contributors accepting paid work plus links to OpenCollective and GitHub Sponsors.
How do I run Biome on a build server?
With the biome ci command, which writes nothing and returns a non-zero exit code on violations. For integration with a change request view, --reporter=github helps, and the formats gitlab, junit, checkstyle, sarif and json are available as well.
Documentation lives on the Biome site, the migration guide in the guides section, and the source code in the GitHub repository.