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

Optional Presets

Opt-in Ultracite presets for ESLint parity, TSDoc, anti-slop rules, shadcn design systems and gdp-ts authorization proofs, and how to set each one up.

Ultracite’s core and framework presets are on when you run init. The presets on this page are opt-in: each adds rules that suit some projects and not others, and most run through Oxlint’s JS plugin API, which is slower than Oxlint’s native rules. The JS plugin, TSDoc, anti-slop and @shadcn/lint presets are for Oxlint; the gdp-ts preset works with Oxlint and ESLint.

ESLint Parity

The core preset — and the react, next, and tanstack framework presets — run entirely on Oxlint’s native, Rust-powered rules for maximum speed. If you want to close the remaining gap with the ESLint preset, Ultracite ships an opt-in js-plugins preset that runs eslint-plugin-github, eslint-plugin-sonarjs, and oxlint-plugin-react-doctor through Oxlint’s JS plugin support. TSDoc syntax and public API documentation checks are available as separate, explicitly selected plugins below; they are not added to the full preset, so existing users do not gain new dependencies when upgrading.

This is off by default because it adds dependencies and runs a slower JavaScript lint pass instead of the native Rust one. You can enable it during setup: interactive ultracite init lets you choose individual JS plugins when you choose Oxlint, and non-interactive setup accepts package names via --js-plugins.

npx ultracite init --linter oxlint --js-plugins eslint-plugin-github eslint-plugin-sonarjs oxlint-plugin-react-doctor
pnpm dlx ultracite init --linter oxlint --js-plugins eslint-plugin-github eslint-plugin-sonarjs oxlint-plugin-react-doctor
yarn dlx ultracite init --linter oxlint --js-plugins eslint-plugin-github eslint-plugin-sonarjs oxlint-plugin-react-doctor
bunx ultracite init --linter oxlint --js-plugins eslint-plugin-github eslint-plugin-sonarjs oxlint-plugin-react-doctor
nubx ultracite init --linter oxlint --js-plugins eslint-plugin-github eslint-plugin-sonarjs oxlint-plugin-react-doctor
aube dlx ultracite init --linter oxlint --js-plugins eslint-plugin-github eslint-plugin-sonarjs oxlint-plugin-react-doctor

React Doctor’s framework-specific rules are split into per-framework add-on presets so they only run where they apply — nextjs-* rules like nextjs-no-img-element fire on plain <img>/<a> JSX and would false-positive in non-Next.js apps. init wires these up automatically when you pick the matching framework together with oxlint-plugin-react-doctor:

  • ultracite/oxlint/next/js-plugins — React Doctor’s Next.js rules
  • ultracite/oxlint/tanstack/js-plugins — React Doctor’s TanStack Query/Router/Start rules

If you’re adding the preset manually instead of through init, install the plugins yourself:

npm install -D eslint-plugin-github eslint-plugin-sonarjs oxlint-plugin-react-doctor
pnpm add -D eslint-plugin-github eslint-plugin-sonarjs oxlint-plugin-react-doctor
yarn add -D eslint-plugin-github eslint-plugin-sonarjs oxlint-plugin-react-doctor
bun add -D eslint-plugin-github eslint-plugin-sonarjs oxlint-plugin-react-doctor
nub add -D eslint-plugin-github eslint-plugin-sonarjs oxlint-plugin-react-doctor
aube add -D eslint-plugin-github eslint-plugin-sonarjs oxlint-plugin-react-doctor
import { defineConfig } from "oxlint";
import core from "ultracite/oxlint/core";
import jsPlugins, { jsPluginSettings } from "ultracite/oxlint/js-plugins";

export default defineConfig({
  extends: [core, jsPlugins],
  ignorePatterns: core.ignorePatterns,
  jsPlugins: jsPlugins.jsPlugins,
  settings: jsPluginSettings,
});

In a Next.js or TanStack app, add the matching add-on preset as well:

import { defineConfig } from "oxlint";
import core from "ultracite/oxlint/core";
import next from "ultracite/oxlint/next";
import jsPlugins, { jsPluginSettings } from "ultracite/oxlint/js-plugins";
import nextJsPlugins from "ultracite/oxlint/next/js-plugins";

export default defineConfig({
  extends: [core, next, jsPlugins, nextJsPlugins],
  ignorePatterns: core.ignorePatterns,
  jsPlugins: jsPlugins.jsPlugins,
  settings: jsPluginSettings,
});

TSDoc and Public API Documentation

Two more JS plugins check the documentation in TypeScript files (.ts, .tsx, .mts, and .cts). They aren’t part of the full js-plugins preset, so they only run when you select them:

  • eslint-plugin-tsdoc, registered as tsdoc, validates TSDoc syntax.
  • eslint-plugin-jsdoc, registered as jsdoc-js because Oxlint reserves jsdoc for its native plugin, requires a doc comment on every exported function, class, interface, type alias, and enum. It also covers the public methods and getters of exported classes, and exports that wrap a function, such as export const Button = forwardRef(...).

Selecting eslint-plugin-jsdoc also turns on Oxlint’s native jsdoc/require-param and jsdoc/require-returns, so every JSDoc comment on a function, exported or not, must describe its parameters and return value. Parameter and return types stay off, so TypeScript types aren’t repeated in comments.

Selecting eslint-plugin-tsdoc adjusts core’s native jsdoc/* rules to fit TSDoc: tags such as @remarks and @typeParam are allowed, and generators don’t need @yields. With both plugins selected, a destructured parameter is documented by its own name (@param props - ...) rather than property by property. TSDoc has no {Type} syntax, so write the type on a @throws tag as a link: @throws {@link RangeError} When the input is empty.

npx ultracite init --linter oxlint --js-plugins eslint-plugin-tsdoc eslint-plugin-jsdoc
pnpm dlx ultracite init --linter oxlint --js-plugins eslint-plugin-tsdoc eslint-plugin-jsdoc
yarn dlx ultracite init --linter oxlint --js-plugins eslint-plugin-tsdoc eslint-plugin-jsdoc
bunx ultracite init --linter oxlint --js-plugins eslint-plugin-tsdoc eslint-plugin-jsdoc
nubx ultracite init --linter oxlint --js-plugins eslint-plugin-tsdoc eslint-plugin-jsdoc
aube dlx ultracite init --linter oxlint --js-plugins eslint-plugin-tsdoc eslint-plugin-jsdoc

--js-plugins sets the whole selection, so list any plugins you already use, such as eslint-plugin-github. A config that extends the full js-plugins preset keeps its plugins.

If you’re configuring Oxlint manually, install the plugins and select them:

npm install -D eslint-plugin-tsdoc eslint-plugin-jsdoc
pnpm add -D eslint-plugin-tsdoc eslint-plugin-jsdoc
yarn add -D eslint-plugin-tsdoc eslint-plugin-jsdoc
bun add -D eslint-plugin-tsdoc eslint-plugin-jsdoc
nub add -D eslint-plugin-tsdoc eslint-plugin-jsdoc
aube add -D eslint-plugin-tsdoc eslint-plugin-jsdoc
import { defineConfig } from "oxlint";
import core from "ultracite/oxlint/core";
import { selectJsPlugins } from "ultracite/oxlint/js-plugins";

const jsPlugins = selectJsPlugins(["tsdoc", "jsdoc-js"]);

export default defineConfig({
  extends: [core, jsPlugins],
  ignorePatterns: core.ignorePatterns,
  jsPlugins: jsPlugins.jsPlugins,
});

Anti-Slop Rules

Ultracite also ships an opt-in anti-slop preset built on anti-slop, Dillon Mulroy’s set of opinionated Oxlint rules that reject low-evidence, low-signal TypeScript and JavaScript patterns — unjustified type assertions, unknown leaking through function signatures, Reflect-based property access, module mocking, and similar escape hatches.

Upstream is deliberately not published to npm (its distribution model is “copy the source into your repo”), so Ultracite vendors a self-contained bundled build of the plugin — there is nothing extra to install. Like js-plugins, this preset is off by default because it is strongly opinionated and runs through Oxlint’s slower JS-plugin pass.

You can enable it during setup: it appears as anti-slop in the same JS-plugins prompt as the packages above, and non-interactive setup accepts it via --js-plugins:

npx ultracite init --linter oxlint --js-plugins anti-slop
pnpm dlx ultracite init --linter oxlint --js-plugins anti-slop
yarn dlx ultracite init --linter oxlint --js-plugins anti-slop
bunx ultracite init --linter oxlint --js-plugins anti-slop
nubx ultracite init --linter oxlint --js-plugins anti-slop
aube dlx ultracite init --linter oxlint --js-plugins anti-slop

Or add it to your config manually:

import { defineConfig } from "oxlint";
import antiSlop from "ultracite/oxlint/anti-slop";
import core from "ultracite/oxlint/core";

export default defineConfig({
  extends: [core, antiSlop],
  ignorePatterns: core.ignorePatterns,
});

All fifteen rules are enabled at the error level, including require-safety-comment-for-type-assertion (every as assertion needs a SAFETY: comment stating the checked invariant), no-unknown-returns / no-unknown-parameters (parse values into named domain types at boundaries), and no-module-mocking (inject dependencies instead of mocking modules). If a rule doesn’t fit your codebase — no-module-mocking is a common example in test-heavy projects — turn it off in your own config’s rules block.

Design System Rules with @shadcn/lint

Ultracite also ships an opt-in shadcn preset built on @shadcn/lint, shadcn’s agent-first linter for Tailwind v4 design systems. It reads your components, variants, and theme, and when a class breaks the design system the error explains what is wrong and what to use instead — the component’s variants, sizes, theme tokens, and the file to edit. shadcn/ui is not required: it works with any Tailwind v4 component directory and theme.

Like js-plugins, this preset is off by default because it adds a dependency and runs through Oxlint’s slower JS-plugin pass. You can enable it during setup: it appears as @shadcn/lint in the same JS-plugins prompt as the packages above, and non-interactive setup accepts it via --js-plugins:

npx ultracite init --linter oxlint --js-plugins @shadcn/lint
pnpm dlx ultracite init --linter oxlint --js-plugins @shadcn/lint
yarn dlx ultracite init --linter oxlint --js-plugins @shadcn/lint
bunx ultracite init --linter oxlint --js-plugins @shadcn/lint
nubx ultracite init --linter oxlint --js-plugins @shadcn/lint
aube dlx ultracite init --linter oxlint --js-plugins @shadcn/lint

Or add it to your config manually:

npm install -D @shadcn/lint
pnpm add -D @shadcn/lint
yarn add -D @shadcn/lint
bun add -D @shadcn/lint
nub add -D @shadcn/lint
aube add -D @shadcn/lint
import { defineConfig } from "oxlint";
import core from "ultracite/oxlint/core";
import shadcn from "ultracite/oxlint/shadcn";

export default defineConfig({
  extends: [core, shadcn],
  ignorePatterns: core.ignorePatterns,
  jsPlugins: shadcn.jsPlugins,
});

All six rules are enabled at the error level:

Rule What it catches
no-restyle Restyling a design-system component through className.
no-raw-colors Raw palette colors such as bg-pink-500 and undeclared tokens.
no-arbitrary-values Arbitrary values such as p-[13px] when a scale value exists.
no-inline-styles Inline style props and <style> elements.
no-unknown-classes Classes your Tailwind cannot generate, such as rounded-huge.
require-static-classes Component class values the linter cannot read, such as `bg-${color}`.

no-restyle and no-arbitrary-values use the upstream recommended allow: ["layout"] policy: pages may place a component (mt-4, w-full, flex-1, absolute) but its padding, colors, typography, and shape stay in the component’s variants. The preset also turns no-restyle, no-arbitrary-values, and require-static-classes off inside **/components/ui/**, where components style themselves and call their own variant functions; no-raw-colors, no-inline-styles, and no-unknown-classes stay on there.

Components and the theme are discovered from components.json, or from components/ui / src/components/ui next to the nearest package.json. If your design system lives elsewhere, point the plugin at it with settings.shadcn on your root config — Oxlint does not merge settings from extended configs — and add a matching override for that directory:

export default defineConfig({
  extends: [core, shadcn],
  ignorePatterns: core.ignorePatterns,
  jsPlugins: shadcn.jsPlugins,
  settings: {
    shadcn: { ui: "@workspace/ui/components" },
  },
  overrides: [
    {
      files: ["packages/ui/src/components/**"],
      rules: {
        "shadcn/no-arbitrary-values": "off",
        "shadcn/no-restyle": "off",
        "shadcn/require-static-classes": "off",
      },
    },
  ],
});

Per-component policies (contracts), custom messages, and exceptions for external stylesheets are configured on the rules themselves — see the @shadcn/lint documentation. no-unknown-classes asks your installed Tailwind v4 which classes exist; when Tailwind or the theme cannot be loaded it falls back to a bundled grammar and warns once.

Authorization Proofs with gdp-ts

Ultracite also ships an opt-in gdp preset for gdp-ts, a TypeScript take on Ghosts of Departed Proofs. Sensitive functions demand a proof about their exact arguments (UserIsProjectAdmin<U, P>) that only a trusted module in proofs/ can mint, so a skipped or mismatched authorization check is a compile error. TypeScript can’t stop a proof being forged with {} as UserIsProjectAdmin<U, P>; these rules close that gap.

The lint plugin ships inside @gdp-ts/core, so once your codebase uses gdp-ts there is nothing extra to install. Add the preset after core:

import { defineConfig } from "oxlint";
import core from "ultracite/oxlint/core";
import gdp from "ultracite/oxlint/gdp";

export default defineConfig({
  extends: [core, gdp],
  ignorePatterns: core.ignorePatterns,
});

The preset follows upstream’s strict mode:

Rule Applies What it catches
gdp-ts/no-define-proof outside proofs/ Importing or calling defineProof to mint a proof outside a trusted module.
gdp-ts/no-proof-assertion outside proofs/ Type assertions to Named, Proof, or any type imported from a proofs path.
gdp-ts/no-type-assertion outside proofs/ and lib/ids.ts Every other type assertion except as const. The honest path never needs one.
gdp-ts/no-exported-prover inside proofs/ Exporting a prover, which would let any module mint the proof.

Upstream’s strict-mode no-any is left off because core’s typescript/no-explicit-any already bans any everywhere. Inside proofs/, typescript/no-empty-interface and typescript/no-empty-object-type allow an empty interface that extends a single type, which is how gdp-ts declares a proof (interface UserIsProjectAdmin<U, P> extends Proof<"UserIsProjectAdmin", [U, P]> {}). Core’s no-redeclare stays on, so give the prover its own name (const prover = defineProof("UserIsProjectAdmin")) instead of reusing the proof interface’s name as the upstream recipe does.

The preset follows the upstream layout: trusted modules in proofs/ and branded-id constructors in lib/ids.ts. For a different layout, add overrides with the same shape in your own config.

On ESLint

The same preset ships for ESLint. Spread it after core:

import core from "ultracite/eslint/core";
import gdp from "ultracite/eslint/gdp";

export default [...core, ...gdp];

It matches the Oxlint preset: upstream’s strict mode stops proofs being forged with type assertions or minted outside proofs/, and empty proof interfaces (interface X<P> extends Proof<"X", [P]> {}) are allowed inside proofs/. It also sets @typescript-eslint/no-unused-vars to ignore _-prefixed arguments, as Oxlint does by default, because sensitive functions take proofs they never read at runtime (_proof: UserIsProjectAdmin<U, P>).

Last updated on

Was this page helpful?