---
title: "Code style"
description: "The conventions a reviewer will hold you to. The named-props-interface rule, what the formatter owns, and the handful of choices that are not negotiable."
url: "https://opsinjs.pensievelabs.org/handbook/contributing/code-style"
source: "https://opsinjs.pensievelabs.org/handbook/contributing/code-style.md"
section: "Handbook"
kind: "handbook"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["code conventions", "style guide", "prettier config", "lint config", "house style"]
---

> Elements written as `<PascalCase … />` below are opsinjs documentation
> components. Their attributes are the content: the values they render are
> generated from `tokens/*.json` and `registry/catalogue.ts` and are
> published separately at https://opsinjs.pensievelabs.org/r/index.json and under the Reference
> section.
> Nothing is missing from this page. The data simply does not live in
> the prose.

<PageTemplate kind="handbook" />

## The short version [#the-short-version]

Formatting is not a discussion. Prettier owns it, ESLint owns correctness, and a
review comment about a line break is a bug in the configuration rather than a
bug in the change.

What is left is a short list of decisions the tools cannot make:

* **A named exported props interface per component.** Non-negotiable. The API
  tables are generated from it.
* **Erasable-syntax-only TypeScript in `scripts/*.mts`.** No enums, no parameter
  properties, no namespaces; plain `node` runs these by type-stripping.
* **No raw colour values, anywhere.**
* **British spelling in prose, American in code identifiers and CSS.**
* **Named exports.** One default export per module is one export you cannot find
  by searching.

## How it works [#how-it-works]

### What the tools own [#what-the-tools-own]

* **Prettier** owns formatting, plus Tailwind class ordering. The Tailwind
  plugin needs `"tailwindStylesheet": "./app/globals.css"` in `.prettierrc` or
  class sorting silently does nothing under Tailwind v4.
* **ESLint** runs `eslint-config-next` plus the project rules. Generated output
  is excluded: `.source/`, `lib/generated/`, `registry/__index__.ts`,
  `public/r/`. Without those exclusions a clean clone lints upstream's generated
  code and reports errors nobody can act on.
* **TypeScript** runs with `strict: true`. `allowImportingTsExtensions` is on
  because the `.mts` scripts import `lib/color/*.ts` with the explicit extension
  Node requires; `noEmit` makes it legal.

`app/globals.css` and `app/product.css` are in `.prettierignore` deliberately.
The formatter reflows the `linear()` spring tokens to one stop per line and
rewrites the block the shadcn CLI produced, which destroys the ability to diff
that file against a fresh `shadcn init`. In a file whose *order* is its content,
that is a bad trade.

### The conventions the tools cannot check [#the-conventions-the-tools-cannot-check]

* **One component per file**, named for the file. See [Naming
  conventions](../naming-conventions.mdx).
* **Props destructured in the signature**, with `className` and `...props`
  last, so a caller's `className` reaches `cn()` in the right position.
* **Early return over nested conditionals.** A component with three levels of
  ternary in its JSX is a component that wants to be two components.
* **JSDoc on every public prop.** It is not a comment; it becomes the
  description column of a published table.
* **Comments explain why, not what.** The what is in the code; the why is the
  thing that gets lost.

## Do this [#do-this]

* **Run `pnpm run check` before opening a pull request.** It runs four things:
  `check:generated`, `check:ia`, `check:a11y` and `check:llms`. It does not run
  `pnpm typecheck` or `pnpm lint`, and CI runs those as separate steps along with
  `pnpm build`, so run those too. The four together are what the pull-request
  gate asks.
* **Name the interface `<Component>Props` and export it.** This is the one
  convention that has a visible consequence in public if you get it wrong: an
  empty API table.
* **Put `className` last in a prop spread** you are forwarding.
* **Prefer a plain function to a class.** There are no classes in this
  codebase, and adding one to a `.mts` script would also break type-stripping.
* **Keep `.mts` scripts dependency-free.** They run under plain `node` with no
  build step; that is a property worth protecting.

## Not this [#not-this]

* **Do not add a formatter or lint rule for personal preference.** Every rule
  costs everyone.
* **Do not use `any`.** `unknown` plus a narrowing is the same amount of typing
  and does not disable the compiler.
* **Do not use a default export.** The exception is where a framework requires
  one: a Next.js page, a route handler, a config file.
* **Do not use an `enum`.** It is not erasable syntax and it will fail at
  runtime in a `.mts` script, with an error that does not mention enums.
* **Do not disable a lint rule for a file.** Disable it for a line, with a
  reason.
* **Do not commit a generated file you edited by hand.** CI regenerates and
  diffs.

## Gotchas [#gotchas]

* **Tailwind class sorting no-ops without `tailwindStylesheet`.** No warning; the
  classes simply stay as typed and a reviewer notices before the tool does.
* **`eslint` 9.x prints a deprecation notice.** Every 9.x release now does,
  because 10.x is `latest`. It is a deprecation notice, not a peer warning, and
  the pin is deliberate. See the note in the root `package.json`.
* **A clean clone fails `typecheck` before `pnpm install`.** `.source/` is
  gitignored and regenerated by `postinstall`, so the type checker points at
  `lib/source.ts` for a problem that is not there.
* **`allowImportingTsExtensions` without `noEmit` is TS5097**, and the error
  points at the import rather than at the configuration.
* **Prettier and the CSS import order disagree by design.** That is why the two
  stylesheets are ignored; do not "fix" it by removing the ignore.
* **A `.mts` script on Node 20 fails with a syntax error.** The version guard at
  the top of each script exists to turn that into a readable message.

## Related [#related]

* [Naming conventions](../naming-conventions.mdx) is the published naming
  contract.
* [TypeScript](../typescript.mdx) covers the named-props-interface rule and what
  the generator does with it.
* [Component checklist](./component-checklist.mdx) is where these conventions
  appear as ship criteria.
* [Contributing](./index.mdx) covers the routes in and the reviews each one
  needs.
