---
title: Optional Presets
description: 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`](https://github.com/github/eslint-plugin-github), [`eslint-plugin-sonarjs`](https://github.com/SonarSource/eslint-plugin-sonarjs), and [`oxlint-plugin-react-doctor`](https://www.npmjs.com/package/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`.

```package-install
npx ultracite init --linter oxlint --js-plugins eslint-plugin-github eslint-plugin-sonarjs oxlint-plugin-react-doctor
```

:::note
The React Doctor rules used to be bundled into the `react`, `next`, and `tanstack` presets. They now live in `js-plugins` so those framework presets stay on the fast native path. Extend `js-plugins` alongside your framework preset to keep them.
:::

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:

```package-install
npm install -D eslint-plugin-github eslint-plugin-sonarjs oxlint-plugin-react-doctor
```

```ts title="oxlint.config.ts"
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,
});
```

:::note
`jsPlugins: jsPlugins.jsPlugins` re-declares the plugin packages on your root config. Oxlint already loads them from the extended preset, but dependency analyzers such as [Knip](https://knip.dev) only read `jsPlugins` from the root config and would otherwise report the packages as unused devDependencies. `ultracite init` emits this line for you.
:::

:::note
`settings: jsPluginSettings` must live on your root config — Oxlint does not merge `settings` from extended configs. It keeps React Doctor's ported rules (such as `only-export-components`) in their framework-aware "curated" mode, so Next.js route-segment exports like `export const dynamic = "force-static"` are not flagged as non-component exports.
:::

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

```ts title="oxlint.config.ts"
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](https://tsdoc.org) 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.`

```package-install
npx 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:

```package-install
npm install -D eslint-plugin-tsdoc eslint-plugin-jsdoc
```

```ts title="oxlint.config.ts"
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](https://github.com/dmmulroy/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`:

```package-install
npx ultracite init --linter oxlint --js-plugins anti-slop
```

Or add it to your config manually:

```ts title="oxlint.config.ts"
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`](https://github.com/shadcn-ui/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`:

```package-install
npx ultracite init --linter oxlint --js-plugins @shadcn/lint
```

Or add it to your config manually:

```package-install
npm install -D @shadcn/lint
```

```ts title="oxlint.config.ts"
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:

```ts title="oxlint.config.ts"
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](https://github.com/shadcn-ui/lint/blob/main/docs/rules.md). `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](https://github.com/rauchg/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:

```ts title="oxlint.config.ts"
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:

```javascript title="eslint.config.mjs"
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>`).
