Configuration
Understand Ultracite's default presets, how framework-specific layers work, and which overrides to keep when your project needs custom rules.
One of Ultracite’s best features is that it’s zero-config out of the box — you can just extend the base preset and get going. However, every project is different, and you might want to customize certain rules or adapt Ultracite for different frameworks.
Framework Presets
Ultracite provides framework-specific configurations that you can extend in addition to the base core configuration. This allows you to add framework-specific linting rules without bloating the base config or dealing with irrelevant rules.
The concept is the same across all supported linters — pull in the core preset, then layer on any framework presets you need — though the exact syntax differs per linter (Biome uses an extends array, while ESLint and Oxlint use imports; each provider page shows its config files). For example, if you’re using Biome with React and Next.js, you can pull in the following presets:
{
"extends": [
"ultracite/biome/core",
"ultracite/biome/react",
"ultracite/biome/next",
],
}
On Oxlint, init writes oxlint.config.ts and oxfmt.config.ts in an ES module package ("type": "module" in package.json) and oxlint.config.mts and oxfmt.config.mts otherwise, because Node only loads ES module syntax from a .ts file in an ES module package. init never changes "type".
For the opt-in presets, such as the JS plugins that bring Oxlint closer to the ESLint preset, see Optional Presets.
Default Configuration
When you include the core preset, you’re pulling in Ultracite’s base configuration. This preset includes framework-agnostic rules and settings. Some notable aspects:
- TypeScript strictness: Enables strict checks and lint rules requiring robust typing. Discourages use of
any, requires handling ofnull/undefined, and prefers explicit types where needed. - Best practices: Common rules like no unused vars, no explicit eval, no prototype pollution, etc.
- Accessibility: Biome’s core preset enables its a11y rules (ARIA attributes, semantic HTML, keyboard navigation and more) for every project. On Oxlint and ESLint, the JSX a11y rules come with the
react,solidandqwikpresets, and ESLint’sastro,svelte,vueandangularpresets check those templates too. In Qwik projects the rules that look at event handlers don’t yet recognise handlers ending in$(onClick$), so they report nothing for them. ESLint’s core preset adds a few that apply to every file, such aseslint-plugin-github’sa11y-*rules. - TODO comments: Oxlint and ESLint report
TODO,FIXME,XXXand@todocomments throughno-warning-comments. Biome has no rule for them. - Formatting conventions: 2-space indentation, 80-character line width, semicolons, trailing commas, and double quotes.
All these defaults aim to enforce consistent style and prevent common errors without manual configuration.
Type-Aware Rules
Rules that read type information, or scan the whole project, are slower than the rest and need more setup, so every linter keeps them out of core. Turn them on at setup time with npx ultracite init --type-aware, which also turns on strictNullChecks in your tsconfig files.
ESLint
The type-aware preset adds the @typescript-eslint, SonarJS and Unicorn rules that need a TypeScript program, such as no-floating-promises, no-unsafe-return and strict-boolean-expressions, and points the parser at your tsconfig files with the project service. core lints TypeScript without them, so it needs no tsconfig. Add type-aware straight after core:
import core from "ultracite/eslint/core";
import typeAware from "ultracite/eslint/type-aware";
import vitest from "ultracite/eslint/vitest";
import vitestTypeAware from "ultracite/eslint/vitest/type-aware";
export default [...core, ...typeAware, ...vitest, ...vitestTypeAware];
Some framework plugins have type-aware rules of their own, which live in an add-on after the framework preset: ultracite/eslint/<framework>/type-aware for Angular, Jest, NestJS, Qwik, TanStack and Vitest. init --type-aware adds the add-ons for the frameworks you pick.
Biome
The type-aware preset enables project-graph and type-aware rules: noPrivateImports, noUnresolvedImports, noDeprecatedImports, useJsonImportAttributes, and useArraySortCompare. These rules have Biome’s scanner read more of the project, so they’re shipped as a separate layer. Two project rules, noImportCycles and noUndeclaredDependencies, are in the core preset: core skips node_modules, so they add no measurable time (#843).
{
"extends": ["ultracite/biome/core", "ultracite/biome/type-aware"],
}
Oxlint
Oxlint runs the type-aware rules core lists, such as typescript/no-floating-promises, through oxlint-tsgolint. init --type-aware installs it and sets typeAware in the config’s options.
Safe Fixes (Biome)
With Biome, ultracite fix runs biome check --write, which applies every rule’s safe fixes. Fixes that Biome marks as unsafe, because they can change behavior, only run with ultracite fix --unsafe. The Biome preset sets "fix": "safe" on a few rules whose fixes Biome marks unsafe by default, so plain ultracite fix applies them too. These fixes are non-breaking in practice:
| Rule | Description |
|---|---|
noUnusedImports |
Removes unused import statements |
useBlockStatements |
Wraps single-line control flow bodies in curly braces |
useTailwindSortedClasses |
Sorts Tailwind CSS classes into a consistent order |
To stop ultracite fix applying one of these, override the rule in your config without the fix property (its fix goes back to Biome’s default and only runs with --unsafe), or set "fix": "none" to turn the fix off entirely.
Overriding Rules
Each linter has its own way to disable or modify rules. For example, to disable the useButtonType rule for Biome, you can do the following:
{
"extends": ["ultracite/biome/core"],
"linter": {
"rules": {
"a11y": {
"useButtonType": "off",
},
},
},
}
Recipes
Excluding Files (Biome)
Ultracite’s base config uses files.includes to match all files while excluding common build outputs and generated code. If you need to exclude additional files or directories, add them to files.includes in your biome.jsonc with negated patterns:
{
"extends": ["ultracite/biome/core"],
"files": {
"includes": ["**", "!components/ui/**", "!prisma/client", "!public"],
},
}
Important: Always include "**" as the first entry. Using only negated patterns (e.g. ["!public", "!prisma/client"]) will result in zero files being matched, because Biome needs at least one positive glob to start from.
Browser Targets (ESLint)
compat/compat reports browser and JavaScript APIs your target browsers don’t support. It reads the targets from your browserslist config. Without one, Ultracite checks against Baseline Widely available browsers, so set your own when you support older or fewer browsers:
{
"browserslist": ["> 0.5%", "last 2 versions", "not dead"]
}
A .browserslistrc file works too. Ultracite looks for the config from the folder ESLint runs in, so in a monorepo that sets targets per package, run ESLint in each package or add targets at the root. For code that only runs on a server, turn the rule off with { rules: { "compat/compat": "off" } }.
Enforcing Path Aliases (Oxlint)
Ultracite’s core preset does not assume a project layout, so it does not force @/* (or #/*) aliases over relative imports — that convention depends on your tsconfig.json, and it would false-positive in monorepos, libraries, and projects without a src directory. If your project maps @/* to src/* and you want to ban ../ imports between source modules, add an override scoped to src:
import { defineConfig } from "oxlint";
import core from "ultracite/oxlint/core";
export default defineConfig({
env: core.env,
extends: [core],
ignorePatterns: core.ignorePatterns,
overrides: [
{
files: ["src/**"],
rules: {
"import/no-relative-parent-imports": "error",
},
},
],
});
import/no-relative-parent-imports is a native Oxlint rule: it flags ../ in imports, type imports, and re-exports at any depth, while still allowing ./sibling imports within a directory. Scoping it to src/** keeps tests, scripts, and config files that legitimately reach into src unaffected.
If you would rather point people at the alias in the diagnostic itself, use no-restricted-imports with a pattern instead:
overrides: [
{
files: ["src/**"],
rules: {
"no-restricted-imports": [
"error",
{
patterns: [
{
regex: "^\\.\\./",
message: "Use the @/* alias for imports between src modules.",
},
],
},
],
},
},
],
Scoping Rules Away from Angular Templates (ESLint)
The angular preset lints .html files and inline templates with Angular’s template parser. It turns the core rules off for them, apart from github/filenames-match-regex, because rules written for JavaScript can’t read a template’s syntax tree. A whole page, such as src/index.html, is linted as a template too, but without the three rules that only fit a template Angular compiles: prefer-ngsrc, no-inline-styles and prefer-self-closing-tags.
A rule you set without files applies to templates again. Some such rules report there: jsdoc/require-file-overview, for example, reports every template as missing @file. Others throw on Angular syntax that shares a name with JavaScript’s: github/js-class-name, github/unescaped-html-literal and sonarjs/code-eval throw on a template literal in a template. Give such an entry, or any rule meant for your TypeScript, a files pattern that leaves templates out:
import angular from "ultracite/eslint/angular";
import core from "ultracite/eslint/core";
export default [
...core,
...angular,
{
files: ["**/*.ts"],
rules: {
"jsdoc/check-tag-names": ["error", { definedTags: ["internal"] }],
},
},
];
In the same way, files: ["**/*.html"] now names templates, so it no longer reaches the inline scripts of an HTML file. They’re linted as a block of their own: settings for them, such as extra browser globals, need files: ["**/*.html/*.htm"].
Sorting Object Keys
Ultracite doesn’t sort object keys. The sorting rules fix what they find, so ultracite fix would reorder every object, and key order can change what code does: the bytes JSON.stringify writes, the order a loop visits keys, which entry of an exports map wins, and what TypeScript infers for an options object. The research post has the cases we measured.
To sort keys anyway, turn the rule on in your own config:
- Oxlint and ESLint:
"sort-keys": "error". - Biome:
"useSortedKeys": "on"underassist.actions.source.
Restricting Imports, Globals and Types
A few rules are on but report nothing until you list what to restrict, since that depends on your project:
- Oxlint:
no-restricted-imports,no-restricted-globalsandtypescript/no-restricted-types. - ESLint: the same three (
@typescript-eslint/no-restricted-types), plusno-restricted-syntax,no-restricted-exports,import-x/no-restricted-paths,import-x/prefer-namespace-import,n/no-restricted-importandn/no-restricted-require. - Biome:
noRestrictedImportsandnoRestrictedTypes.noRestrictedGlobalsalways banseventanderror, and you can add more.
Give one options in your config to use it:
rules: {
"no-restricted-imports": [
"error",
{
paths: [
{
name: "lodash",
message: "Import from lodash-es, which tree-shakes.",
},
],
},
],
},
Sorting Classes from a Custom Tailwind Theme (Oxfmt)
Tailwind class sorting uses the same algorithm as prettier-plugin-tailwindcss. If you use Tailwind CSS v4 with a custom theme, point Oxfmt at your stylesheet so sorting respects your custom utilities:
import { defineConfig } from "oxfmt";
import ultracite from "ultracite/oxfmt";
export default defineConfig({
...ultracite,
sortTailwindcss: {
functions: ["clsx", "cva", "tw", "twMerge", "cn", "twJoin", "tv"],
stylesheet: "./app/globals.css",
},
});
Setting sortTailwindcss replaces the preset’s value instead of merging with it, so keep the preset’s functions list.
Oxfmt resolves stylesheet relative to the config file that sets it. A path that doesn’t resolve turns sorting off without an error, so check that classes still sort after you change it. In a monorepo with a single root config, use the path from the root, such as ./apps/web/src/styles.css.
Oxfmt formats each file with its nearest config and doesn’t merge it with the root one, so a workspace with an Oxfmt config of its own doesn’t get the root’s sortTailwindcss. The same goes for apps with different themes, since stylesheet takes one file. Give each of those workspaces a config that spreads the root config and points at its own stylesheet:
import { defineConfig } from "oxfmt";
import root from "../../oxfmt.config.ts";
export default defineConfig({
...root,
sortTailwindcss: {
functions: ["clsx", "cva", "tw", "twMerge", "cn", "twJoin", "tv"],
stylesheet: "./src/styles.css",
},
});
If the root config is oxfmt.config.mts, import that file instead. In a workspace whose package.json doesn’t set "type": "module", name the workspace config oxfmt.config.mts.
Formatting Astro Files (Oxfmt)
Oxfmt formats .astro files only when its astro option is set, through a copy of prettier-plugin-astro it ships with. That plugin loads @astrojs/compiler-rs from your project, and without it every .astro file fails to format, so Ultracite’s preset leaves astro unset. When init sets up Astro, for the whole project or for one workspace, it turns the option on in the root config and installs the compiler:
import { defineConfig } from "oxfmt";
import ultracite from "ultracite/oxfmt";
export default defineConfig({
...ultracite,
astro: true,
});
To set this up by hand, add astro: true. Astro 7 and later depend on the compiler already; on earlier versions, also install @astrojs/compiler-rs as a dev dependency. Set astro: false to leave .astro files alone. init keeps the astro value you set, including an object of plugin options.
Oxfmt also needs to know which whitespace Astro keeps, or the line breaks it adds between tags can show up as spaces on the page. Its default matches Astro 7, which drops whitespace that contains a line break. Astro 6 and earlier collapse that whitespace to a space, as browsers do, so for them init writes astro: { compressHTML: "html" } instead. Change it to astro: true when you upgrade to Astro 7. If your Astro config sets compressHTML, match it in Oxfmt’s: true is "html", false is "none", and "jsx" is the default.
Adding Third-Party Plugins (Oxlint)
Oxlint supports community plugins for additional rules beyond the built-in set. For example, Ultracite’s Biome preset includes cognitive complexity checking via noExcessiveCognitiveComplexity, but Oxlint only has the standard cyclomatic complexity rule built in. Ultracite’s opt-in js-plugins preset (init --js-plugins) covers it with sonarjs/cognitive-complexity. If you’d rather not load that preset, a third-party plugin does the same job, like oxlint-plugin-complexity:
npm install -D oxlint-plugin-complexitypnpm add -D oxlint-plugin-complexityyarn add -D oxlint-plugin-complexitybun add -D oxlint-plugin-complexitynub add -D oxlint-plugin-complexityaube add -D oxlint-plugin-complexityimport { defineConfig } from "oxlint";
import core from "ultracite/oxlint/core";
export default defineConfig({
env: core.env,
extends: [core],
ignorePatterns: core.ignorePatterns,
jsPlugins: ["oxlint-plugin-complexity"],
rules: {
"complexity/complexity": ["error", { cognitive: 15 }],
},
});
This approach lets you opt in to additional checks without Ultracite taking on third-party dependencies in its core preset.
Effect (Oxlint)
Effect’s type-aware diagnostics — floating Effects, unhandled error and requirement channels, Layer and Scope mistakes, and Effect-native alternatives to console, Date, process.env, and friends — are available to Oxlint through @effect/tsgo. Ultracite doesn’t ship an Effect preset because the effecttsgo plugin isn’t part of stock Oxlint: @effect/tsgo replaces the installed oxlint and oxlint-tsgolint binaries with builds that include it, and only supports specific versions of each. Layer it on top of core yourself:
npm install -D @effect/tsgo oxlint-tsgolintpnpm add -D @effect/tsgo oxlint-tsgolintyarn add -D @effect/tsgo oxlint-tsgolintbun add -D @effect/tsgo oxlint-tsgolintnub add -D @effect/tsgo oxlint-tsgolintaube add -D @effect/tsgo oxlint-tsgolintPatch the binaries after every install with a prepare script (--no-typescript limits the patch to Oxlint; drop it if you also want Effect’s TypeScript-Go integration):
{
"scripts": {
"prepare": "effect-tsgo patch --no-typescript --oxlint"
}
}
Then extend one of the presets @effect/tsgo ships:
import { strict } from "@effect/tsgo/oxlint-presets";
import { defineConfig } from "oxlint";
import core from "ultracite/oxlint/core";
export default defineConfig({
env: core.env,
extends: [core, strict],
ignorePatterns: core.ignorePatterns,
rules: {
"effecttsgo/global-console-in-effect": "error",
"effecttsgo/global-date-in-effect": "error",
},
});
strict raises every Effect diagnostic that is on by default to error; use recommended instead to keep upstream’s severities. Both presets turn on Oxlint’s type-aware mode, so --type-aware isn’t needed — and core’s type-aware rules, such as typescript/no-floating-promises, run too. Upstream leaves some rules off, like the *-in-effect family above; enable the ones you want in rules, but skip effecttsgo/strict-boolean-expressions, which duplicates core’s typescript/strict-boolean-expressions. Some rules only apply to Effect 3 or Effect 4 and do nothing on the other major version — see the rule list.
A few things to keep in mind:
- Unpatched Oxlint fails the whole run with
Unknown plugin: 'effecttsgo', so the patch has to be in place everywhere you lint, CI included. - Reinstalling or upgrading
oxlintoroxlint-tsgolint(including throughultracite upgrade) can restore the stock binaries without re-runningprepare. Runnpx effect-tsgo patch --no-typescript --oxlintagain afterwards. - The patch refuses versions it doesn’t support, which fails the install. Check the supported versions in the
@effect/tsgoREADME before upgrading either package. - Effect’s fixes are reported as suggestions, so
ultracite fixonly applies them with--unsafe.
Rule Reference
Every rule each preset sets, and why some are off, is in the presets’ source on GitHub: Oxlint, Biome and ESLint.