.babelrc.json and babel.config.json: Complete Reference and Modern Alternatives
Last updated:
Babel still ships in millions of projects, but the landscape around it has shifted — SWC compiles the same code 20-70x faster, esbuild bundles in a fraction of the time, and Next.js dropped Babel as the default compiler back in version 12. If you are maintaining a Babel project today, the choice between .babelrc.json and babel.config.json is the first decision: the former is project-relative and walks up the directory tree from each input file, the latter is a root config that applies to every file Babel sees including dependencies in node_modules. Both files use the same field shape — presets, plugins, env, targets, overrides — and both are strict JSON, so they share the same parsing quirks. This reference walks through every field, explains the ordering rules that trip up most teams, and shows the migration path to SWC for projects ready to leave Babel behind.
A broken Babel config usually fails with a single confusing line number. Paste your .babelrc.json or babel.config.json into Jsonic's JSON Validator to catch trailing commas, missing quotes, and bracket mismatches before Babel does.
Is Babel still needed in 2026? (TS native, SWC, esbuild context)
The honest answer for new projects: probably not. TypeScript compiles itself, Node.js 22+ runs modern ESM and most stage-4 syntax natively, every evergreen browser supports ES2022, and the bundlers that used to call Babel internally now ship their own faster transpilers. Vite uses esbuild for dev and SWC (or Rollup with esbuild) for production. Next.js compiles with SWC. Bun has its own transpiler. Deno needs no transpiler at all.
Babel still has a place in three scenarios. Legacy maintenance: existing webpack builds, older React projects on Create React App ejects, and enterprise codebases with custom Babel plugins are not worth rewriting just to swap compilers. Custom AST transforms: Babel's plugin ecosystem is still the largest — if you need a specific transform like babel-plugin-styled-components or a codemod runner, Babel remains the practical tool. Browser support deeper than the SWC default range: if you must ship code that runs on IE11 (rare but real for some intranets), Babel with @babel/preset-env and a tuned browserslist is still the most complete option.
For everything else, the rational default in 2026 is SWC, esbuild, or no transpilation at all. Test the assumption with your own browserslist — if your targets are evergreen-only, you may discover you can drop the entire compile step and ship modern JS directly.
.babelrc.json vs babel.config.json: which to use when
The two formats look identical inside but behave differently outside. The difference is scope: which files Babel applies the config to.
.babelrc.json is project-relative. Babel starts from the file being compiled, walks up the directory tree, and uses the first .babelrc.json (or .babelrc) it finds. It stops at the nearest package.json, and it never applies inside node_modules. This is the right choice for monorepos where each workspace package has its own Babel pipeline — package A can ship a React+TS config while package B uses plain modern JS, and neither leaks into the other.
babel.config.json is a root config. There is exactly one per repo, it lives at the directory you tell Babel is the root (usually the repo root, set by rootMode or auto-detected), and it applies to every file Babel sees — including dependencies in node_modules. Pick this when you need to transform code inside dependencies (a common need for legacy packages that ship modern syntax without a build step), or when you want one config to govern the whole repo without per-directory variation.
| Aspect | .babelrc.json | babel.config.json |
|---|---|---|
| Lookup | Up directory tree | Root only |
| Scope | Per-package | Whole repo |
| Applies inside node_modules? | No | Yes |
| Multiple per repo? | Yes (one per package) | No (one only) |
| Best for | Monorepos with per-workspace config | Repos that transform dependencies or want one global config |
If both files exist, babel.config.json is loaded first and .babelrc.json files extend it for their packages — the two compose rather than conflict.
presets array: @babel/preset-env, preset-react, preset-typescript
A preset is a named bundle of plugins that adds a coherent set of features. The three presets that cover most modern projects are @babel/preset-env, @babel/preset-react, and @babel/preset-typescript.
@babel/preset-env replaces the older year-specific presets (preset-es2015, preset-es2016, etc.). You set browser targets and preset-env figures out which syntax transforms and polyfills your code needs. Without targets, preset-env transforms everything down to ES5 — usually not what you want in 2026. Pair it with a targets field (covered in section 6) or a browserslist entry in package.json.
@babel/preset-react transforms JSX. Since React 17 it accepts a runtime: "automatic" option that imports the JSX runtime from react/jsx-runtime automatically, so you no longer need import React from 'react' at the top of every file. Set development: true in dev for better error messages.
@babel/preset-typescript strips TypeScript syntax without doing any type-checking. This is fast but it means Babel cannot catch type errors — run tsc --noEmit separately or rely on your IDE for type validation. The preset handles isolated-modules transpilation, so it cannot transform const enum or namespaces by default. Set allowDeclareFields: true for class field declarations.
{
"presets": [
["@babel/preset-env", { "useBuiltIns": "usage", "corejs": 3 }],
["@babel/preset-react", { "runtime": "automatic" }],
"@babel/preset-typescript"
]
}Each preset entry is either a string (just the package name) or a two-element array of [name, options]. Babel resolves package names by prepending @babel/preset- when the entry looks like a short name.
plugins array: ordering, options, plugin-X vs babel-plugin-X
Plugins are individual transforms. They run before presets and in the order they appear in the array — top to bottom. The first plugin sees your raw source code; each subsequent plugin sees the output of the previous one.
The naming convention is two-tier. Official Babel plugins are scoped under @babel/ — @babel/plugin-transform-runtime, @babel/plugin-proposal-decorators, and so on. Community plugins use the babel-plugin- prefix without a scope: babel-plugin-styled-components, babel-plugin-module-resolver. Babel's resolver handles both styles automatically — you write the short name or full package name and Babel finds it.
{
"plugins": [
["@babel/plugin-proposal-decorators", { "version": "2023-05" }],
["@babel/plugin-transform-runtime", { "corejs": 3 }],
"babel-plugin-styled-components",
["babel-plugin-module-resolver", {
"root": ["./src"],
"alias": { "@components": "./src/components" }
}]
]
}Plugin options follow the same [name, options] tuple as presets. When ordering matters — for example, decorators must run before class properties — list the dependent plugins in the required order, top to bottom. Babel does not reorder plugins based on metadata; the array order is the execution order.
A common pitfall: ordering between plugins and presets is fixed (plugins first, presets second), but ordering withinplugins differs from ordering within presets. Plugins go top-to-bottom; presets go bottom-to-top. The next section's FAQ has the rationale.
env field: per-environment overrides (test, production)
The env field lets you switch parts of the config based on the current Babel environment. Babel reads BABEL_ENV first, falls back to NODE_ENV, and defaults to "development". Each key under env is a config fragment that gets merged on top of the top-level config when that environment is active.
{
"presets": [
["@babel/preset-env", { "targets": "> 0.5%, last 2 versions, not dead" }],
"@babel/preset-react"
],
"env": {
"test": {
"presets": [
["@babel/preset-env", { "targets": { "node": "current" } }]
]
},
"production": {
"plugins": ["babel-plugin-transform-remove-console"]
}
}
}Two common patterns. Test environment targeting Node: Jest sets NODE_ENV=test, and you usually want to compile against the current Node version (which supports nearly all modern syntax) rather than browser targets. This dramatically speeds up test runs because Babel skips most transforms. Production stripping: add babel-plugin-transform-remove-console or similar dead-code plugins only in production builds to keep dev builds fast and debugging easy.
Merge semantics matter. The env fragment replaces array fields like presets and plugins for the matching environment, but other top-level fields (like sourceMaps) carry over from the parent. If you want to add a plugin in production rather than replace the whole array, put the shared plugins at the top level and only the env-specific extras under env.production.plugins.
targets and browserslist integration
The targets field tells Babel which environments your output must support. Without it, @babel/preset-env transforms aggressively to ES5 — producing larger, slower bundles than nearly any modern project needs. With realistic targets, preset-env skips transforms that all your browsers already support natively.
{
"targets": "> 0.5%, last 2 versions, Firefox ESR, not dead",
"presets": ["@babel/preset-env"]
}Targets accepts three shapes: a browserslist query string (as above), an object mapping target names to versions ({ "chrome": "100", "node": "20" }), or an esmodules: true shortcut that targets only modern browsers with native ES modules support.
The recommended approach is to put your browser support policy in package.json under the browserslist field and let Babel, autoprefixer, and other tools share the same list. See the package.json browserslist field guide for the syntax. When browserslist is set in package.json and targets is unset in the Babel config, preset-env reads browserslist automatically.
For libraries published to npm, set targets narrower than your consuming apps — ship modern JS and let the app's build step transform further if needed. For Node-only code (server-side, build scripts), use { "node": "current" } so preset-env compiles to whatever the local Node version supports, which is usually nothing.
overrides: per-file pattern config
The overrides field applies extra config to files matching a glob pattern. Each override is an object with a test (or include/exclude) pattern plus the same field shape as the root config — presets, plugins, targets, env.
{
"presets": ["@babel/preset-env", "@babel/preset-react"],
"overrides": [
{
"test": ["./src/legacy/**/*.js"],
"presets": [["@babel/preset-env", { "targets": "ie 11" }]]
},
{
"test": ["./src/server/**/*.ts"],
"presets": [
["@babel/preset-env", { "targets": { "node": "20" } }],
"@babel/preset-typescript"
]
},
{
"test": "**/*.test.ts",
"plugins": ["babel-plugin-rewire"]
}
]
}Overrides are the right tool when you need different Babel behavior for different parts of the repo without splitting into multiple .babelrc.json files. Common uses: tighter targets for a legacy folder that still ships to old browsers, Node-only config for server code in a full-stack repo, test-specific plugins like rewire or transform-modules-commonjs that should not affect production builds.
The merge semantics: an override's fields are applied on top of the root config, with arrays replaced (not concatenated). If you want to add a plugin to a file pattern rather than replace the plugin list, you have to repeat the root plugins in the override.
For library authors, overrides is also the way to apply different transforms to TypeScript declaration files versus runtime code, or to skip transforms entirely on generated files by listing them under exclude.
Migrating from Babel to SWC (.swcrc) or esbuild
SWC is a Rust-based compiler that reads a config file called .swcrc and produces output similar to Babel's for the same input. For the canonical React+TypeScript Babel config in this guide, the SWC equivalent is roughly half the size and runs 20-70x faster.
// .swcrc — equivalent of the babel.config.json above
{
"jsc": {
"parser": {
"syntax": "typescript",
"tsx": true,
"decorators": true
},
"transform": {
"react": {
"runtime": "automatic",
"development": false
}
},
"target": "es2020"
},
"env": {
"targets": "> 0.5%, last 2 versions, not dead"
},
"module": {
"type": "es6"
}
}Replace the bundler integration. In webpack, swap babel-loader for swc-loader. In Vite, use @vitejs/plugin-react-swc instead of the default React plugin. For Jest, install @swc/jest and set transform to { "^.+\.(t|j)sx?$": "@swc/jest" }.
esbuild is even faster but covers less ground — it transforms TypeScript and modern JS to a target ES version but does not run user-defined transforms. Use esbuild when you need the maximum speed and have no plugin requirements. Use SWC when you need a Babel-equivalent feature set with Babel-style configurability.
The migration gap is custom Babel plugins. If you depend on a plugin without an SWC equivalent (some MDX, codemod, or proprietary transforms), either keep Babel for that file pattern using a webpack rule, or rewrite the transform as an SWC plugin in Rust. Most popular plugins (styled-components, emotion, lodash imports) already have SWC native ports.
Babel vs SWC vs esbuild
| Aspect | Babel | SWC | esbuild |
|---|---|---|---|
| Language | JavaScript | Rust | Go |
| Relative speed | 1x (baseline) | 20-70x | 50-100x |
| Config file | .babelrc.json / babel.config.json | .swcrc | CLI flags or build script |
| Plugin ecosystem | Largest (thousands) | Growing (hundreds) | Limited (transforms only) |
| TypeScript stripping | Yes (preset-typescript) | Yes (native) | Yes (native) |
| JSX | Yes (preset-react) | Yes (native) | Yes (native) |
| Type-checking | No | No | No |
| Where Next.js uses it | Fallback when .babelrc exists | Default compiler since v12 | Internal in some build steps |
| Where Vite uses it | Optional via plugin | Optional via plugin-react-swc | Default for dev transforms |
The practical takeaway: for new projects, default to SWC or esbuild and reach for Babel only when a specific plugin or syntax level requires it. For existing Babel projects, the migration is usually a one-day swap with measurable build time wins.
Key terms
- .babelrc.json
- A project-relative Babel config file. Babel walks up the directory tree from each input file, applies the nearest
.babelrc.json, and never crosses intonode_modules. Right choice for monorepos with per-package Babel pipelines. - babel.config.json
- A root Babel config file. One per repo at the directory Babel treats as the root, applies to every file Babel processes — including dependencies inside
node_modules. Right choice when you need to transform dependency code or want a single global config. - preset
- A named bundle of Babel plugins shipped as one package. Presets run after plugins and in reverse array order — the last preset listed runs first.
@babel/preset-env,@babel/preset-react, and@babel/preset-typescriptcover most modern projects. - plugin
- An individual Babel transform. Plugins run before presets and in array order — the first plugin listed runs first. Official plugins use the
@babel/plugin-prefix; community plugins usebabel-plugin-. - targets
- The environments Babel must compile for. Accepts a browserslist query, an object mapping target names to versions, or the
esmodules: trueshortcut. Drives which transforms@babel/preset-envapplies. - SWC
- A Rust-based JS/TS compiler that reads
.swcrcand replaces Babel for most use cases. Runs 20-70x faster than Babel and ships as the default compiler in Next.js since v12. Configured similarly to Babel with jsc/parser/transform sections.
Frequently asked questions
Is .babelrc deprecated?
The plain .babelrc file (no extension) is not formally deprecated in Babel 7.x — it still works — but the Babel team recommends one of two replacements. Use .babelrc.json when you want a project-relative config that follows the up-the-tree lookup rule (each package can have its own). Use babel.config.json when you want a root-level config that applies to the whole repo including transforms inside node_modules. Both .json variants give you stricter parsing and clearer editor support than the extensionless .babelrc, and both work identically in current toolchains (webpack, Rollup, Vite, Jest, the Babel CLI). The bigger trend is away from Babel entirely — Next.js dropped it as the default compiler in v12 (2021) for SWC, Vite and esbuild handle modern JS without a Babel pipeline, and Bun has its own transpiler. If you are starting a new project in 2026, you may not need a Babel config file at all.
What's the difference between .babelrc.json and babel.config.json?
They differ in scope and lookup behavior. .babelrc.json is a project-relative config: Babel walks up from each input file looking for the nearest .babelrc.json, stops at the first one (or at a package.json boundary), and never applies to files outside that package. This makes per-package config in a monorepo easy — each workspace can have its own .babelrc.json with different presets. babel.config.json is a root config: it must sit at the directory you pass to Babel as the root (usually the repo root), and it applies to every file Babel processes, including dependencies inside node_modules. Reach for babel.config.json when you need to transform code in dependencies (common for legacy package compatibility or for transforming TS in linked workspace packages), and use .babelrc.json when each package owns its own transform pipeline.
Why does my .babelrc not apply inside node_modules?
By design — .babelrc and .babelrc.json are project-relative configs and Babel deliberately ignores them inside node_modules. The intent is to stop your project config from breaking dependencies that ship pre-compiled code expecting their own Babel setup (or no Babel at all). If you genuinely need to transform a dependency — for example, to convert an untranspiled ESM-only package to CommonJS for older bundlers, or to apply a class-properties plugin to a library that ships modern syntax — switch to babel.config.json at the repo root. That format ignores the node_modules boundary and applies to everything Babel sees. The trade-off is that one global config now controls dependency transforms, so be conservative: scope plugins with overrides patterns or env conditions to avoid breaking dependencies that already work.
Can I configure Babel in package.json?
Yes — Babel reads a top-level "babel" key in package.json with the exact same shape as a .babelrc.json file. This is convenient for small projects with two or three preset entries and no plugins, because you avoid an extra file in the repo root. The downsides: package.json gets noisier, JSON inside package.json cannot have comments (same constraint as .babelrc.json), and tools that read package.json for unrelated reasons (publish, install) may parse the babel section unnecessarily. For anything beyond a single preset, prefer a dedicated .babelrc.json or babel.config.json file — the editor gets better autocomplete from the file extension, and version-control diffs stay focused on config changes. The package.json route is roughly equivalent to a .babelrc (project-relative scope), so the same node_modules exclusion applies.
How do I migrate from Babel to SWC?
Create a .swcrc at the repo root with the equivalent compile targets. For a typical React+TypeScript Babel config (preset-env + preset-react + preset-typescript), the SWC equivalent is { "jsc": { "parser": { "syntax": "typescript", "tsx": true }, "transform": { "react": { "runtime": "automatic" } }, "target": "es2020" } }. SWC handles TypeScript, JSX, and modern syntax in one pass and runs 20-70x faster than Babel. Then swap the bundler integration: replace babel-loader with swc-loader in webpack, or use @vitejs/plugin-react-swc in Vite. For Jest, install @swc/jest and set transform to { ".(js|ts|tsx)": "@swc/jest" }. Most plugin needs (decorators, class properties, optional chaining) are built into SWC. The gap is custom Babel plugins — if you depend on one without an SWC port, either stay on Babel for that file or rewrite the transform.
Why are presets ordered reverse from plugins?
Babel applies plugins first and presets last, with two different ordering rules within each group. Plugins run in array order, top to bottom — the first plugin in your config sees the original source, the next plugin sees what the previous one produced, and so on. Presets are essentially named bundles of plugins, and they run in reverse array order, bottom to top — the last preset in the array runs first. The rationale is composability: a preset higher in the array (closer to "your code") often customizes a preset below it (the base), so the base must run first to set up the syntax the customizing preset modifies. The practical rule: list presets in the order you logically think about them ("typescript, then react, then env") and Babel reverses the application, which usually matches intuition once you see a config or two.
What replaces babel-polyfill in 2026?
babel-polyfill has been deprecated since Babel 7.4 (2019) and removed from current docs. The modern replacement is two pieces: install core-js@3 and regenerator-runtime as dependencies, then let @babel/preset-env inject only the polyfills your code actually uses based on your browser targets. Configure it with { "useBuiltIns": "usage", "corejs": 3 } in your preset-env options. This produces smaller bundles than the old all-or-nothing babel-polyfill because Babel scans your source and pulls in only the features the targets miss. For library authors who should not inject polyfills into consumer apps, use @babel/plugin-transform-runtime with corejs: 3 instead — it rewrites references to use core-js without polluting globals. As of 2026, most modern browser targets need very few polyfills; if you support only evergreen browsers, you can set useBuiltIns to false and skip polyfills entirely.
Does Next.js still use Babel?
No — not by default. Next.js 12 (December 2021) replaced Babel with SWC as the default compiler, and every major version since has deepened that switch. SWC handles JSX, TypeScript, and modern syntax in Next.js builds with no .babelrc.json or babel.config.json required. The only time Next.js falls back to Babel is when it detects one of those files in your project — its presence opts you out of SWC for compilation, which slows builds 5-10x and disables some Next.js features (like the styled-components SWC transform and the Edge runtime optimizations). If you have a leftover .babelrc.json in a Next.js project that you do not need, delete it to get SWC back. If you must keep a Babel plugin (a transform with no SWC equivalent), accept the slower build or scope the Babel config narrowly with overrides so most files still go through SWC.
Related references
- JSON config files overview — the broader landscape of JSON-based tool config and when to choose each format.
- package.json browserslist field — share browser targets between Babel, autoprefixer, and other tools.
- .prettierrc reference — companion config for code formatting that pairs with Babel-based linting setups.
- tsconfig.json reference — TypeScript compiler options that run alongside
@babel/preset-typescriptfor type-checking. - .eslintrc.json reference — the sibling lint config that historically shared parsers with Babel through
@babel/eslint-parser.
Further reading and primary sources
- Babel Docs — Config Files — Official reference for .babelrc.json, babel.config.json, and the lookup rules
- Babel Docs — @babel/preset-env — Targets, useBuiltIns, corejs, and the modern polyfill story
- SWC Configuration — Full .swcrc reference with the Babel-equivalent field mapping
- Next.js Compiler — How Next.js uses SWC and the fallback to Babel when a Babel config is detected
- esbuild — JavaScript Loader — Target options and transform features for the fastest of the three compilers