Skip to content
Ultracite
Esc
↑↓navigate↵open⌘Jpreview

ESLint, Prettier and Stylelint, set up as one

Ultracite turns on 918 rules from ESLint and ten plugins, then sets up Prettier and Stylelint to match. Three short config files extend Ultracite’s, so the only rules you maintain are your own.

Also onOxlintBiomeChecked against ESLint 10.12

import core from "ultracite/eslint/core";918 rules
import react from "ultracite/eslint/react";549 rules
import next from "ultracite/eslint/next";47 rules

export default [
  ...core,
  ...react,
  ...next,
];
1,514 rules
import config from "ultracite/prettier";

export default {
  ...config,80 columns, double quotes
  plugins: ["prettier-plugin-tailwindcss"],
};
export { default } from "ultracite/stylelint";standard + property order

726 rules recommended. 918 with Ultracite.

ESLint runs nothing until you configure it, and every plugin’s recommended config is a separate thing to install and keep up. Ultracite’s core preset turns on most of the rules in ten plugins, and an opt-in preset adds the TypeScript rules that need your types.

Rules in ESLint 10 and the eight plugins drawn here
1,190
In their recommended configs, if you set each one up yourself
726
On in Ultracite’s core preset, before any framework rules
918
In the plugin’s recommended configTurned on by UltraciteLeft off
  • unicornModern, idiomatic JavaScript

    346 / 374

  • sonarjsBugs and code smells, from SonarQube

    234 / 282

  • eslintESLint’s own rules

    164 / 199

  • @typescript-eslintTypeScript, with 62 more in type-aware

    57 / 127

  • jsdocJSDoc comments

    19 / 78

  • import-xImports and exports

    28 / 46

  • nNode.js modules and APIs

    33 / 43

  • githubGitHub’s own conventions

    21 / 24

  • promisePromises

    14 / 17

Errors, never warnings

All 918 are errors, because an agent takes a zero exit code as done, and a warning doesn’t change it. That includes compat/compat, which flags browser APIs your browserslist targets don’t support, or Baseline’s when you set none.

132 rules that read your types

Core works from the syntax alone, without a tsconfig.json. The type-aware preset adds 100 typescript-eslint, SonarJS and Unicorn rules that need TypeScript’s type checker, such as no-floating-promises, and hands it to 32 SonarJS rules in core that use it when it’s there. It’s opt-in, with init --type-aware.

ESLint 10.12.0 and Ultracite 8.0.0, counted with eslint --print-config on a TypeScript file. Plugins are drawn to one scale. Not drawn: unused-imports/no-unused-imports and compat/compat, one rule each.

Fifteen framework presets, templates included

Each preset sets up the framework’s own ESLint plugin. For Vue, Svelte and Astro that includes its parser, so ESLint checks the markup, not just the script block. Six also have a type-aware add-on for their plugin’s rules that read types.

Rules per preset

Add the ones you use
  • react · React, hooks, jsx-a11y, React Doctor549

  • vue · eslint-plugin-vuetemplates208

  • vitest · @vitest/eslint-plugin70+1

  • svelte · eslint-plugin-sveltetemplates69

  • jest · eslint-plugin-jest63+4

  • astro · eslint-plugin-astrotemplates53

  • next · @next/eslint-plugin-next, React Doctor47

  • angular · @angular-eslint45+6

  • solid · eslint-plugin-solid34

  • tanstack · Query, Router, React Doctor34+2

  • nestjs · eslint-plugin-nestjs-typed22+3

  • storybook · eslint-plugin-storybook16

  • cypress · eslint-plugin-cypress12

  • qwik · eslint-plugin-qwik11+1

  • remix · eslint-plugin-remix2

+n rules in the preset’s type-aware add-on, which init --type-aware adds after it.

<script setup lang="ts">
defineProps<{ tasks: { id: string; title: string; notes: string }[] }>();
</script>

<template>
  <ul>
    <li v-for="task in tasks">
      <span v-html="task.notes"></span>
      {{ task.title }}
    </li>
  </ul>
</template>

Terminal

$ npx ultracite check components/task-list.vue

All matched files use Prettier code style!

2:15error The "tasks" property should have a JSDoc comment vue/require-prop-comment

7:5error Elements in iteration expect to have 'v-bind:key' directives vue/require-v-for-key

8:13error 'v-html' directive can lead to XSS attack vue/no-v-html

Prettier formats it. ESLint finds one more.

The same component as on the other engines’ pages. ESLint’s autofix sorts the imports, then Prettier settles the formatting and class order. One rule finds a problem no autofix can settle, and that goes back to the agent.

As written

import { cn } from '@/lib/utils'
import { Slot } from '@radix-ui/react-slot'
import type { ReactNode } from 'react'

export function Button({ asChild, children }: { asChild?: boolean, children: ReactNode }) {
  const Comp = asChild ? Slot : 'button'
  return <Comp className={cn('text-sm px-4 bg-primary hover:bg-primary/90 rounded-md h-10 font-medium')}>{children}</Comp>
}

After ultracite fix

import { Slot } from "@radix-ui/react-slot";
import { cn } from "@/lib/utils";
import type { ReactNode } from "react";

export function Button({
  asChild,
  children,
}: {
  asChild?: boolean;
  children: ReactNode;
}) {
  const Comp = asChild ? Slot : "button";
  return (
    <Comp
      className={cn(
        "bg-primary hover:bg-primary/90 h-10 rounded-md px-4 text-sm font-medium"
      )}
    >
      {children}
    </Comp>
  );
}

Terminal

$ npx ultracite fix components/button.tsx

5:8error Expected a function expression func-style

✖ 1 problem (1 error, 0 warnings)

components/button.tsx 58ms

prettier-plugin-tailwindcss
Tailwind classes in the plugin’s order, in className and in cn, clsx, cva, tw, twMerge, twJoin and tv calls.
import-x/order
Imports grouped by ESLint’s autofix: built-ins, packages, your own modules, then type-only imports.
eslint-config-prettier
Turns off the ESLint rules that would fight Prettier. fix runs ESLint’s fixes first and Prettier last, so Prettier settles the layout.
ultracite/eslint/type-aware
Opt-in rules that read your types. With it, strict-boolean-expressions also flags the optional asChild in the condition.

The trade-off is time

ESLint and its plugins run in Node, and Prettier and Stylelint each run as a step of their own. That’s seconds where the native engines take a third of one. In an agent’s loop it’s less, because the hook only fixes the file that changed. The type-aware preset adds TypeScript’s type checker, which took the 420 files to 19.7 s.

42 files

ESLint + Prettier + Stylelint

3.91 s

Oxlint + Oxfmt

340 ms

Biome

257 ms

420 files

ESLint + Prettier + Stylelint

15.5 s

Oxlint + Oxfmt

344 ms

Biome

379 ms
To check the one file an agent just edited
2.2s
To check 42 files
3.91s
To check 420 files
15.5s
Medians of five runs of ultracite check on an Apple M1 Pro, through each toolchain’s core preset: ESLint 10.12.0, Prettier 3.9.10 and Stylelint 17.16.0; Oxlint 1.87.0 and Oxfmt 0.71.0; and Biome 2.5.15, on the repo’s benchmark fixture copied to 42 and 420 files. Times include starting Node and the CLI.

The same rules in your editor, your agent and CI

init points your editor at ESLint and Prettier and tells your agents how to run them, so a file passes or fails the same way wherever it’s checked.

  1. {
      "editor.codeActionsOnSave": {
        "source.fixAll.eslint": "explicit",
        "source.organizeImports": "explicit"
      },
      "editor.defaultFormatter": "esbenp.prettier-vscode",
      "editor.formatOnSave": true
    }

    VS Code, Cursor, Devin Desktop

    Fix and format on save

    ESLint applies its fixes and sorts imports when you save, and Prettier formats the file, through the editors’ own ESLint and Prettier extensions.

  2. "TSX": {
      "code_actions_on_format": {
        "source.fixAll.eslint": true,
        "source.organizeImports.eslint": true
      },
      "formatter": "prettier"
    }

    Zed

    Fix as you format

    Zed applies ESLint’s fixes and sorts imports, then formats with its Prettier integration, using your Prettier config and plugins.

  3. Terminal

    $ npx ultracite check

    [warn] src/data.ts

    src/api.ts

    13:9 error Prefer `AbortSignal.timeout()` over manually aborting an `AbortController` with `setTimeout()` unicorn/prefer-abort-signal-timeout

    src/data.ts

    20:14 error Use `Array#toSorted()` instead of `Array#sort()` unicorn/no-array-sort

    Coding agents

    A count of what fix will settle

    ESLint ends its report with how many of the problems --fix can settle, so an agent knows to run ultracite fix before working through the rest. init writes the rules into AGENTS.md, which all 40 agents read.

ESLint questions, answered

What does Ultracite add on top of ESLint?
ESLint runs no rules until you configure it. Ultracite’s core preset turns on 918 across ESLint and ten plugins, among them typescript-eslint, Unicorn, SonarJS and import-x. Framework presets add more: 549 rules for React, 47 for Next.js and 208 for Vue. The opt-in type-aware preset adds 100 rules that read your types. init also writes Prettier and Stylelint configs, editor settings for VS Code and Zed, AGENTS.md for coding agents, and optional post-edit hooks.
How do I turn off a rule?
Add a config object after the presets in eslint.config.mjs, such as { rules: { "no-console": "off" } }. In a flat config, later objects win. For one line, use // eslint-disable-next-line no-console.
Why is it slower than Oxlint and Biome?
ESLint and its plugins run in Node, and Prettier and Stylelint run as steps of their own. In our benchmark the toolchain checked 42 files in 3.91 s and 420 files in 15.5 s, where Oxlint took 340 ms and 344 ms. Checking the single file an agent edited took 2.2 s. The type-aware preset adds TypeScript’s type checker, which took the 420 files to 19.7 s.
When should I pick ESLint over Oxlint or Biome?
When you depend on something only ESLint has: a plugin with no native port, a Prettier plugin, or template linting for Vue, Svelte and Astro through each framework’s own parser. Otherwise start with Oxlint, which keeps ESLint’s rule names and runs ESLint plugins through its JS plugin API.
Do I need a tsconfig.json?
Not for the core preset, which lints TypeScript from its syntax alone. The type-aware preset does: its rules use projectService, which finds the tsconfig.json that includes each file, and a .ts or .tsx file that no tsconfig.json includes fails with a parsing error. init --type-aware adds the preset and offers to turn on strictNullChecks, which some of its rules need.
Which files does it lint?
ESLint lints JavaScript and TypeScript modules (.js, .jsx, .mjs, .cjs, .ts, .tsx, .mts, .cts) and inline scripts in HTML files, and the Vue, Svelte and Astro presets add their components. It doesn’t lint JSON, which Prettier formats. Stylelint lints .css, .scss and .less, extending stylelint-config-standard and stylelint-config-idiomatic-order, reads SCSS and Less through the postcss-scss and postcss-less syntaxes init installs, and accepts Tailwind v4’s at-rules, functions and theme variables.
Can I use Prettier plugins?
Yes. prettier.config.mjs is an ordinary Prettier config that spreads Ultracite’s settings, so add plugins to its plugins array. init puts prettier-plugin-tailwindcss there, and prettier-plugin-svelte or prettier-plugin-astro before it when you pick those frameworks. Keep the Tailwind plugin last, as its docs require.

Ship less slop

One command sets up your linter, formatter, editor and agents. Works with Oxlint, Biome and ESLint.