Element usage

Keep a route or component file under 500 lines

A file past 500 lines is doing more than one job. Split it — extract the sub-components, move the helpers, lift the data. This is a warning, not an error: the number is a prompt to look, not a law.

Tier 5warnlintkeep-files-readable

Layout is yours. Appearance is the library's.

What this catches

An agent appends. Asked for one more feature it adds it to the file already open, because that is the cheapest edit to make and it never has to scroll the result. Nothing stops the file at 400 lines, or 900, or 2,000 — and by then no human reviews it properly and every later agent edit is working from a worse starting point.

Why

Length is a proxy, and a good one. A 2,000-line route is not bad because of the number; it is bad because it is holding a page, four sub-components, three data transforms and a form, none of which can be read, tested, or reused on their own. The line count is simply the first symptom visible from outside, which is what makes it checkable.

It compounds specifically with agents. Every edit starts by reading the file, so an oversized file makes every future change more expensive and less accurate — more context spent, more chance of an edit landing in the wrong one of four similar blocks. Splitting is the cheapest thing you can do to keep later work correct, and it is nearly free at 500 lines and painful at 2,000.

500 is deliberately generous. This library is 119 components and 117 of them are under it; the two that are not — sidebar and chart — are composite components with many sub-parts, which is the honest case for exceeding the limit rather than a licence to. If a file needs to be longer, say so in the pull request; the warning exists to make that a decision rather than an accident.

Wrong / right

A route that grew four components

Don't

// app/routes/dashboard.tsx — 1,400 lines
export default function Dashboard() { /* … 300 lines of JSX … */ }
function MetricCard() { /* … */ }
function UsageChart() { /* … */ }
function InviteForm() { /* … */ }
const transformUsage = (rows) => { /* … */ }

Do

// app/routes/dashboard.tsx — 90 lines
import { MetricCard } from "@/features/dashboard/metric-card"
import { UsageChart } from "@/features/dashboard/usage-chart"
import { InviteForm } from "@/features/dashboard/invite-form"
import { transformUsage } from "@/features/dashboard/usage"

export default function Dashboard() { /* … the page, and only the page … */ }

Each extracted piece is now testable and reusable on its own, and the next agent asked to change the invite form opens a 60-line file instead of reading 1,400 to find it.

When the limit is the wrong answer

Don't

// One 900-line component split into six files that only ever appear together,
// each importing the other five, none meaningful alone.

Do

// src/components/sidebar.tsx — 888 lines, one cohesive component with its
// sub-parts (SidebarHeader, SidebarContent, SidebarRail, …) that are only ever
// used with each other, and a warning acknowledged in review.

Splitting a genuinely cohesive component into files that cannot stand alone trades one problem for a worse one. That is why this is a warning: it asks the question, and you are allowed to answer no.

How to check this

Add one of these to your project and the rule holds without anyone having to remember it — including the agent writing half the JSX. The exceptions below are already applied, so a documented carve-out will not be reported.

What it will and will not catch: With no linter at all the equivalent is `find src -name '*.tsx' | xargs wc -l | awk '$1 > 500'`, which is why this rule ships no ripgrep check — counting lines is not a pattern match. Biome counts every line, including imports and comments — so a file with a long licence header sits closer to the limit than its code does. It also cannot tell a cohesive 600-line component from an incoherent one; that judgement is the reason this rule warns rather than fails.

Biome — style/noExcessiveLinesPerFile

biome

A built-in Biome rule, so there is nothing to install and no pattern to maintain. One message per element, and the documented exceptions are ordinary `overrides`.

// biome.jsonc
{
  "linter": {
    "rules": {
      "style": {
        "noExcessiveLinesPerFile": {
          "level": "warn",
          "options": {
            "elements": {}
          }
        }
      }
    }
  },
  "overrides": [
    {
      "includes": [
        "src/components/**",
        "components/ui/**"
      ],
      "linter": {
        "rules": {
          "style": {
            "noExcessiveLinesPerFile": "off"
          }
        }
      }
    }
  ]
}

Claiming an exception that is not a path

biome

One exception on this rule is a judgement call, so it cannot be a path. Biome's suppression syntax has a slot for the reason — fill it, because that note is what makes the carve-out reviewable instead of invisible.

{/* biome-ignore lint/style/noExcessiveLinesPerFile: Generated files — A generated module is read by the machine that wrote it. */}

Enforcing more than this one rule? Take the whole config instead of collecting snippets.

Exceptions

Carve-outs are part of the rule, not a way around it. Each one is already an ignore glob in the checks above, so the cases listed here need no disable comment — and a case that is not listed is one to argue for, not to silence.

The ui-lib component source itself, wherever you pasted it (components/ui/**)
You did not write it and cannot split it without forking it. Two of the library's own components are over the limit — sidebar and chart — which is a fair criticism of the library, not something a consumer should be asked to act on in their lint run.
Generated files
A generated module is read by the machine that wrote it. Length says nothing about whether it is well organised, and splitting it means changing the generator for no reader's benefit.

Scope and enforcement

Applies to
  • app/**/*.{tsx,jsx}
  • src/**/*.{tsx,jsx}
Enforced by
lint