enforcement — biome

run the rules, don't just read them.

All 15 rules as one Biome setup, exceptions included. It is rebuilt from the rules whenever they change, so re-download it rather than maintaining a copy by hand — that way a rule we sharpen reaches your CI. Enforcing one rule at a time? Each rule's page carries its own snippet, plus a ripgrep one-liner for projects with no linter at all.

Nothing to install from us beyond Biome itself, which parses TSX with no parser to configure. These keys are a fragment to merge: the config says nothing about Biome's recommended rules, so dropping it in cannot silently change what else your project lints. You own all of it once it lands — soften a rule to warn, scope it with overrides, or drop an entry you disagree with.

wire it up

01
# 1. Biome, if the project does not have it yet
npm i -D @biomejs/biome

# 2. The GritQL plugins — one per rule Biome has no built-in for
mkdir -p ui-lib-rules
curl -o ui-lib-rules/no-appearance-classes-on-layout-elements.grit https://ui-lib.quebi.de/api/rules/plugins/no-appearance-classes-on-layout-elements.grit
curl -o ui-lib-rules/no-hardcoded-design-values.grit https://ui-lib.quebi.de/api/rules/plugins/no-hardcoded-design-values.grit
curl -o ui-lib-rules/format-values-through-the-library.grit https://ui-lib.quebi.de/api/rules/plugins/format-values-through-the-library.grit
curl -o ui-lib-rules/no-nested-card.grit https://ui-lib.quebi.de/api/rules/plugins/no-nested-card.grit
curl -o ui-lib-rules/bind-fields-through-conform.grit https://ui-lib.quebi.de/api/rules/plugins/bind-fields-through-conform.grit
curl -o ui-lib-rules/render-field-text-through-the-field.grit https://ui-lib.quebi.de/api/rules/plugins/render-field-text-through-the-field.grit
curl -o ui-lib-rules/validate-on-the-server-with-the-same-schema.grit https://ui-lib.quebi.de/api/rules/plugins/validate-on-the-server-with-the-same-schema.grit
curl -o ui-lib-rules/seed-toggles-with-default-selected.grit https://ui-lib.quebi.de/api/rules/plugins/seed-toggles-with-default-selected.grit
curl -o ui-lib-rules/gate-last-result-on-idle-navigation.grit https://ui-lib.quebi.de/api/rules/plugins/gate-last-result-on-idle-navigation.grit
curl -o ui-lib-rules/intent-buttons-must-not-be-type-button.grit https://ui-lib.quebi.de/api/rules/plugins/intent-buttons-must-not-be-type-button.grit
curl -o ui-lib-rules/no-client-sorting-on-a-server-driven-table.grit https://ui-lib.quebi.de/api/rules/plugins/no-client-sorting-on-a-server-driven-table.grit

# 3. The config — merge these keys into your biome.jsonc
curl -O https://ui-lib.quebi.de/api/rules/biome.jsonc

npx biome lint src

how biome carries each rule

02

Two mechanisms, and the difference decides how a rule's documented exceptions are applied.

Built-in rules
Interactive elements correctness/noRestrictedElements
Primitive imports style/noRestrictedImports
File length style/noExcessiveLinesPerFile
Browser dialogs suspicious/noAlert

Configured in biome.jsonc, so their exceptions are ordinary overrides — Biome's own path scoping.

GritQL plugins

Each plugin is loaded by an overrides entry of its own, whose includes are the paths the rule applies to followed by its exceptions as ! entries — the same mechanism the built-ins use, from the same records. Load one from the top-level plugins instead and it runs against every file you lint: an override adds a plugin to the files it matches and cannot take one away.

biome.jsonc — all 15 rules

03
/api/rules/biome.jsonc
// AUTO-GENERATED from https://ui-lib.quebi.de/api/rules.json — do not edit by hand.
//
// Every rule, message, and exception below comes from a rule record in quebi
// ui-lib, so this file and https://ui-lib.quebi.de/rules cannot drift apart.
//
// Merge these keys into your own biome.jsonc. The `plugins` entries are
// GritQL files served alongside this one — fetch them into ./ui-lib-rules/:
//   curl -o ui-lib-rules/no-appearance-classes-on-layout-elements.grit https://ui-lib.quebi.de/api/rules/plugins/no-appearance-classes-on-layout-elements.grit
//   curl -o ui-lib-rules/no-hardcoded-design-values.grit https://ui-lib.quebi.de/api/rules/plugins/no-hardcoded-design-values.grit
//   curl -o ui-lib-rules/format-values-through-the-library.grit https://ui-lib.quebi.de/api/rules/plugins/format-values-through-the-library.grit
//   curl -o ui-lib-rules/no-nested-card.grit https://ui-lib.quebi.de/api/rules/plugins/no-nested-card.grit
//   curl -o ui-lib-rules/bind-fields-through-conform.grit https://ui-lib.quebi.de/api/rules/plugins/bind-fields-through-conform.grit
//   curl -o ui-lib-rules/render-field-text-through-the-field.grit https://ui-lib.quebi.de/api/rules/plugins/render-field-text-through-the-field.grit
//   curl -o ui-lib-rules/validate-on-the-server-with-the-same-schema.grit https://ui-lib.quebi.de/api/rules/plugins/validate-on-the-server-with-the-same-schema.grit
//   curl -o ui-lib-rules/seed-toggles-with-default-selected.grit https://ui-lib.quebi.de/api/rules/plugins/seed-toggles-with-default-selected.grit
//   curl -o ui-lib-rules/gate-last-result-on-idle-navigation.grit https://ui-lib.quebi.de/api/rules/plugins/gate-last-result-on-idle-navigation.grit
//   curl -o ui-lib-rules/intent-buttons-must-not-be-type-button.grit https://ui-lib.quebi.de/api/rules/plugins/intent-buttons-must-not-be-type-button.grit
//   curl -o ui-lib-rules/no-client-sorting-on-a-server-driven-table.grit https://ui-lib.quebi.de/api/rules/plugins/no-client-sorting-on-a-server-driven-table.grit
//
// Note that the plugins are listed inside `overrides` and not at the top
// level, and that this is the whole of their scope. A plugin in the
// top-level `plugins` is run against every file Biome lints — an override
// adds one to the files it matches and cannot subtract one already loaded
// globally — so listing them there would silence nothing and widen
// everything. If your project lays its source out differently from app/ or
// src/, edit those `includes` to match; the .grit files themselves say
// nothing about paths.

{
  "linter": {
    "rules": {
      "correctness": {
        "noRestrictedElements": {
          "level": "error",
          "options": {
            "elements": {
              "button": "Use <Button> from @/components/button (performs an action in place), or <LinkButton> from @/components/link-button (navigates — a control that changes the URL must be an anchor), or <Toggle> from @/components/toggle (has an on/off pressed state).",
              "a": "Use <Link> from @/components/link (an inline text link (it renders a plain anchor for http(s)/mailto/tel hrefs, so external links keep working outside a router)), or <LinkButton> from @/components/link-button (a link that should look like a button).",
              "input": "Use <TextField> from @/components/text-field (a labelled field with description and error text), or <Input> from @/components/input (the bare control, inside a Field you compose yourself), or <SearchField> from @/components/search-field (type=search), or <NumberField> from @/components/number-field (type=number — adds steppers and locale-aware parsing), or <Checkbox> from @/components/checkbox (type=checkbox), or <RadioGroup> from @/components/radio (type=radio), or <Switch> from @/components/switch (a boolean rendered as a switch).",
              "select": "Use <Select> from @/components/select (a single choice from a known list), or <ComboBox> from @/components/combo-box (the list is long enough to need typeahead), or <MultipleSelect> from @/components/multiple-select (multiple).",
              "textarea": "Use <Textarea> from @/components/textarea.",
              "form": "Use <Form> from react-router (submitting to a route action), or <ConformField> from @/components/conform-field (binding fields to a Conform form).",
              "label": "Use <Label> from @/components/field (composing a field by hand).",
              "dialog": "Use <Modal> from @/components/modal (a centered overlay), or <Drawer> from @/components/drawer (an edge-anchored panel), or <Sheet> from @/components/sheet (a side sheet), or <Dialog> from @/components/dialog (the dialog content itself (title, body, footer)).",
              "table": "Use <Table> from @/components/table (a static table (with TableHeader, TableBody, TableColumn, TableRow, TableCell)), or <DataTable> from @/components/data-table (the rows you have are all of them — the browser holds the dataset and sorts, filters, pages and groups it), or <ServerTable> from @/components/server-table (the rows are a page of a larger answer — sorting, filtering and paging are queries the server runs), or <TableShell> from @/components/table-shell (neither mode fits and you are bringing a TanStack instance of your own)."
            }
          }
        }
      },
      "style": {
        "noRestrictedImports": {
          "level": "error",
          "options": {
            "paths": {
              "react-aria-components": {
                "importNames": [
                  "Autocomplete",
                  "Breadcrumb",
                  "Breadcrumbs",
                  "Button",
                  "Calendar",
                  "CalendarCell",
                  "CalendarGrid",
                  "CalendarGridBody",
                  "CalendarGridHeader",
                  "CalendarHeaderCell",
                  "CalendarStateContext",
                  "Cell",
                  "Checkbox",
                  "CheckboxContext",
                  "CheckboxGroup",
                  "Collection",
                  "CollectionRendererContext",
                  "ColorArea",
                  "ColorField",
                  "ColorFieldStateContext",
                  "ColorPicker",
                  "ColorPickerStateContext",
                  "ColorSlider",
                  "ColorSwatch",
                  "ColorSwatchPicker",
                  "ColorSwatchPickerItem",
                  "ColorThumb",
                  "ColorWheel",
                  "ColorWheelTrack",
                  "Column",
                  "ColumnResizer",
                  "ComboBox",
                  "DateField",
                  "DateInput",
                  "DatePicker",
                  "DateRangePicker",
                  "DateSegment",
                  "DefaultCollectionRenderer",
                  "Dialog",
                  "DialogTrigger",
                  "Disclosure",
                  "DisclosureGroup",
                  "DisclosurePanel",
                  "DisclosureStateContext",
                  "DropZone",
                  "FieldError",
                  "FieldErrorContext",
                  "FileTrigger",
                  "Form",
                  "GridList",
                  "GridListHeader",
                  "GridListItem",
                  "GridListSection",
                  "Group",
                  "Header",
                  "Heading",
                  "Input",
                  "Keyboard",
                  "Label",
                  "Link",
                  "ListBox",
                  "ListBoxItem",
                  "ListBoxSection",
                  "Menu",
                  "MenuItem",
                  "MenuSection",
                  "MenuTrigger",
                  "Meter",
                  "Modal",
                  "ModalContext",
                  "ModalOverlay",
                  "NumberField",
                  "OverlayArrow",
                  "OverlayTriggerStateContext",
                  "Popover",
                  "Pressable",
                  "ProgressBar",
                  "Radio",
                  "RadioGroup",
                  "RangeCalendar",
                  "RangeCalendarStateContext",
                  "ResizableTableContainer",
                  "Row",
                  "SearchField",
                  "Select",
                  "SelectValue",
                  "Separator",
                  "Slider",
                  "SliderOutput",
                  "SliderStateContext",
                  "SliderThumb",
                  "SliderTrack",
                  "SubmenuTrigger",
                  "Switch",
                  "Tab",
                  "TabList",
                  "TabPanel",
                  "TabPanels",
                  "Table",
                  "TableBody",
                  "TableHeader",
                  "TableLayout",
                  "TableLoadMoreItem",
                  "TableStateContext",
                  "Tabs",
                  "Tag",
                  "TagGroup",
                  "TagList",
                  "Text",
                  "TextArea",
                  "TextField",
                  "TimeField",
                  "ToggleButton",
                  "ToggleButtonGroup",
                  "Toolbar",
                  "Tooltip",
                  "TooltipTrigger",
                  "Tree",
                  "TreeItem",
                  "TreeItemContent",
                  "Virtualizer"
                ],
                "message": "react-aria-components is ui-lib's dependency, not yours: import the quebi component instead (Button -> @/components/button, TextField -> @/components/text-field, and so on). Helpers the library does not wrap, like parseColor, and type-only imports stay allowed. See https://ui-lib.quebi.de/rules/import-components-not-primitives"
              }
            }
          }
        },
        "noExcessiveLinesPerFile": {
          "level": "warn",
          "options": {
            "maxLines": 500
          }
        }
      },
      "suspicious": {
        "noAlert": {
          "level": "error"
        }
      }
    }
  },
  "overrides": [
    {
      "includes": [
        "app/**/*.{tsx,jsx}",
        "src/**/*.{tsx,jsx}",
        // no-raw-interactive-elements — Only <input>, and only there.
        // no-appearance-classes-on-layout-elements — Components are made of appearance-styled divs — that is what a component is.
        // no-hardcoded-design-values — The components resolve palette scales deliberately — a danger state on red-500, a Badge intent on emerald-500 — and they are not yours to re-token.
        // import-components-not-primitives — This is the layer that imports the primitives — it is what makes the components components.
        // keep-files-readable — You did not write it and cannot split it without forking it.
        // format-values-through-the-library — The formatters this rule points at are implemented there: FormattedNumber, FormattedDate and the Calendar's month/year labels format through Intl themselves — via the cached constructors in `@/lib/intl`, which build one formatter per (locale, options) pair instead of one per rendered value — which is what makes the rest of the codebase able not to.
        // bind-fields-through-conform — The conform-* variants are where the metadata is read off the field and put onto a control.
        // render-field-text-through-the-field — The conform-* variants and the field primitives are where this markup is supposed to live — they are the layer that renders the label, the description, and the error, and wires the ids between them.
        // validate-on-the-server-with-the-same-schema — A ui-lib form is a control surface, not a submission.
        // seed-toggles-with-default-selected — That is the layer that reads the metadata off the field and names it onto a control.
        "!src/components/**",
        // no-raw-interactive-elements — Only <input>, and only there.
        // no-appearance-classes-on-layout-elements — Components are made of appearance-styled divs — that is what a component is.
        // no-hardcoded-design-values — The components resolve palette scales deliberately — a danger state on red-500, a Badge intent on emerald-500 — and they are not yours to re-token.
        // import-components-not-primitives — This is the layer that imports the primitives — it is what makes the components components.
        // keep-files-readable — You did not write it and cannot split it without forking it.
        // format-values-through-the-library — The formatters this rule points at are implemented there: FormattedNumber, FormattedDate and the Calendar's month/year labels format through Intl themselves — via the cached constructors in `@/lib/intl`, which build one formatter per (locale, options) pair instead of one per rendered value — which is what makes the rest of the codebase able not to.
        // bind-fields-through-conform — The conform-* variants are where the metadata is read off the field and put onto a control.
        // render-field-text-through-the-field — The conform-* variants and the field primitives are where this markup is supposed to live — they are the layer that renders the label, the description, and the error, and wires the ids between them.
        // validate-on-the-server-with-the-same-schema — A ui-lib form is a control surface, not a submission.
        // seed-toggles-with-default-selected — That is the layer that reads the metadata off the field and names it onto a control.
        "!components/ui/**"
      ],
      "plugins": [
        "./ui-lib-rules/no-appearance-classes-on-layout-elements.grit", // Layout elements are yours — until their classes describe appearance
        "./ui-lib-rules/format-values-through-the-library.grit", // Format numbers and dates through the library, with an explicit locale
        "./ui-lib-rules/seed-toggles-with-default-selected.grit" // Never spread getInputProps onto a Checkbox or Switch
      ]
    },
    {
      "includes": [
        "app/**/*.{tsx,jsx,css}",
        "src/**/*.{tsx,jsx,css}",
        // no-raw-interactive-elements — Only <input>, and only there.
        // no-appearance-classes-on-layout-elements — Components are made of appearance-styled divs — that is what a component is.
        // no-hardcoded-design-values — The components resolve palette scales deliberately — a danger state on red-500, a Badge intent on emerald-500 — and they are not yours to re-token.
        // import-components-not-primitives — This is the layer that imports the primitives — it is what makes the components components.
        // keep-files-readable — You did not write it and cannot split it without forking it.
        // format-values-through-the-library — The formatters this rule points at are implemented there: FormattedNumber, FormattedDate and the Calendar's month/year labels format through Intl themselves — via the cached constructors in `@/lib/intl`, which build one formatter per (locale, options) pair instead of one per rendered value — which is what makes the rest of the codebase able not to.
        // bind-fields-through-conform — The conform-* variants are where the metadata is read off the field and put onto a control.
        // render-field-text-through-the-field — The conform-* variants and the field primitives are where this markup is supposed to live — they are the layer that renders the label, the description, and the error, and wires the ids between them.
        // validate-on-the-server-with-the-same-schema — A ui-lib form is a control surface, not a submission.
        // seed-toggles-with-default-selected — That is the layer that reads the metadata off the field and names it onto a control.
        "!components/ui/**",
        // no-raw-interactive-elements — Only <input>, and only there.
        // no-appearance-classes-on-layout-elements — Components are made of appearance-styled divs — that is what a component is.
        // no-hardcoded-design-values — The components resolve palette scales deliberately — a danger state on red-500, a Badge intent on emerald-500 — and they are not yours to re-token.
        // import-components-not-primitives — This is the layer that imports the primitives — it is what makes the components components.
        // keep-files-readable — You did not write it and cannot split it without forking it.
        // format-values-through-the-library — The formatters this rule points at are implemented there: FormattedNumber, FormattedDate and the Calendar's month/year labels format through Intl themselves — via the cached constructors in `@/lib/intl`, which build one formatter per (locale, options) pair instead of one per rendered value — which is what makes the rest of the codebase able not to.
        // bind-fields-through-conform — The conform-* variants are where the metadata is read off the field and put onto a control.
        // render-field-text-through-the-field — The conform-* variants and the field primitives are where this markup is supposed to live — they are the layer that renders the label, the description, and the error, and wires the ids between them.
        // validate-on-the-server-with-the-same-schema — A ui-lib form is a control surface, not a submission.
        // seed-toggles-with-default-selected — That is the layer that reads the metadata off the field and names it onto a control.
        "!src/components/**",
        // no-hardcoded-design-values — The EU energy-label colour scale is specified by regulation.
        "!src/components/energy-class-badge.tsx",
        // no-hardcoded-design-values — The EU energy-label colour scale is specified by regulation.
        "!components/ui/energy-class-badge.tsx"
      ],
      "plugins": [
        "./ui-lib-rules/no-hardcoded-design-values.grit" // Design values come from quebi tokens, never from literals
      ]
    },
    {
      "includes": [
        "app/**/*.{tsx,jsx}",
        "src/**/*.{tsx,jsx}"
      ],
      "plugins": [
        "./ui-lib-rules/no-nested-card.grit", // A card inside a card is a section
        "./ui-lib-rules/intent-buttons-must-not-be-type-button.grit", // An intent button submits — never type="button"
        "./ui-lib-rules/no-client-sorting-on-a-server-driven-table.grit" // Sorting a server-driven table is a query, not an array method
      ]
    },
    {
      "includes": [
        "app/**/*.{tsx,jsx}",
        "src/**/*.{tsx,jsx}",
        // no-raw-interactive-elements — Only <input>, and only there.
        // no-appearance-classes-on-layout-elements — Components are made of appearance-styled divs — that is what a component is.
        // no-hardcoded-design-values — The components resolve palette scales deliberately — a danger state on red-500, a Badge intent on emerald-500 — and they are not yours to re-token.
        // import-components-not-primitives — This is the layer that imports the primitives — it is what makes the components components.
        // keep-files-readable — You did not write it and cannot split it without forking it.
        // format-values-through-the-library — The formatters this rule points at are implemented there: FormattedNumber, FormattedDate and the Calendar's month/year labels format through Intl themselves — via the cached constructors in `@/lib/intl`, which build one formatter per (locale, options) pair instead of one per rendered value — which is what makes the rest of the codebase able not to.
        // bind-fields-through-conform — The conform-* variants are where the metadata is read off the field and put onto a control.
        // render-field-text-through-the-field — The conform-* variants and the field primitives are where this markup is supposed to live — they are the layer that renders the label, the description, and the error, and wires the ids between them.
        // validate-on-the-server-with-the-same-schema — A ui-lib form is a control surface, not a submission.
        // seed-toggles-with-default-selected — That is the layer that reads the metadata off the field and names it onto a control.
        "!components/ui/**",
        // no-raw-interactive-elements — Only <input>, and only there.
        // no-appearance-classes-on-layout-elements — Components are made of appearance-styled divs — that is what a component is.
        // no-hardcoded-design-values — The components resolve palette scales deliberately — a danger state on red-500, a Badge intent on emerald-500 — and they are not yours to re-token.
        // import-components-not-primitives — This is the layer that imports the primitives — it is what makes the components components.
        // keep-files-readable — You did not write it and cannot split it without forking it.
        // format-values-through-the-library — The formatters this rule points at are implemented there: FormattedNumber, FormattedDate and the Calendar's month/year labels format through Intl themselves — via the cached constructors in `@/lib/intl`, which build one formatter per (locale, options) pair instead of one per rendered value — which is what makes the rest of the codebase able not to.
        // bind-fields-through-conform — The conform-* variants are where the metadata is read off the field and put onto a control.
        // render-field-text-through-the-field — The conform-* variants and the field primitives are where this markup is supposed to live — they are the layer that renders the label, the description, and the error, and wires the ids between them.
        // validate-on-the-server-with-the-same-schema — A ui-lib form is a control surface, not a submission.
        // seed-toggles-with-default-selected — That is the layer that reads the metadata off the field and names it onto a control.
        "!src/components/**"
      ],
      "plugins": [
        "./ui-lib-rules/bind-fields-through-conform.grit", // Bind fields through Conform, never by hand
        "./ui-lib-rules/render-field-text-through-the-field.grit" // Label, description and error come from the field — not from markup beside it
      ]
    },
    {
      "includes": [
        "app/**/*.{tsx,jsx}",
        "src/**/*.{tsx,jsx}",
        // no-raw-interactive-elements — Only <input>, and only there.
        // no-appearance-classes-on-layout-elements — Components are made of appearance-styled divs — that is what a component is.
        // no-hardcoded-design-values — The components resolve palette scales deliberately — a danger state on red-500, a Badge intent on emerald-500 — and they are not yours to re-token.
        // import-components-not-primitives — This is the layer that imports the primitives — it is what makes the components components.
        // keep-files-readable — You did not write it and cannot split it without forking it.
        // format-values-through-the-library — The formatters this rule points at are implemented there: FormattedNumber, FormattedDate and the Calendar's month/year labels format through Intl themselves — via the cached constructors in `@/lib/intl`, which build one formatter per (locale, options) pair instead of one per rendered value — which is what makes the rest of the codebase able not to.
        // bind-fields-through-conform — The conform-* variants are where the metadata is read off the field and put onto a control.
        // render-field-text-through-the-field — The conform-* variants and the field primitives are where this markup is supposed to live — they are the layer that renders the label, the description, and the error, and wires the ids between them.
        // validate-on-the-server-with-the-same-schema — A ui-lib form is a control surface, not a submission.
        // seed-toggles-with-default-selected — That is the layer that reads the metadata off the field and names it onto a control.
        "!src/components/**",
        // no-raw-interactive-elements — Only <input>, and only there.
        // no-appearance-classes-on-layout-elements — Components are made of appearance-styled divs — that is what a component is.
        // no-hardcoded-design-values — The components resolve palette scales deliberately — a danger state on red-500, a Badge intent on emerald-500 — and they are not yours to re-token.
        // import-components-not-primitives — This is the layer that imports the primitives — it is what makes the components components.
        // keep-files-readable — You did not write it and cannot split it without forking it.
        // format-values-through-the-library — The formatters this rule points at are implemented there: FormattedNumber, FormattedDate and the Calendar's month/year labels format through Intl themselves — via the cached constructors in `@/lib/intl`, which build one formatter per (locale, options) pair instead of one per rendered value — which is what makes the rest of the codebase able not to.
        // bind-fields-through-conform — The conform-* variants are where the metadata is read off the field and put onto a control.
        // render-field-text-through-the-field — The conform-* variants and the field primitives are where this markup is supposed to live — they are the layer that renders the label, the description, and the error, and wires the ids between them.
        // validate-on-the-server-with-the-same-schema — A ui-lib form is a control surface, not a submission.
        // seed-toggles-with-default-selected — That is the layer that reads the metadata off the field and names it onto a control.
        "!components/ui/**",
        // validate-on-the-server-with-the-same-schema — A rendered example has no route action to post to, so it validates client-side by definition.
        // gate-last-result-on-idle-navigation — A rendered example has no route action and no navigation to be idle, so there is nothing to gate.
        "!**/*.stories.{tsx,jsx}",
        // validate-on-the-server-with-the-same-schema — A rendered example has no route action to post to, so it validates client-side by definition.
        // gate-last-result-on-idle-navigation — A rendered example has no route action and no navigation to be idle, so there is nothing to gate.
        "!**/*.examples.{tsx,jsx}"
      ],
      "plugins": [
        "./ui-lib-rules/validate-on-the-server-with-the-same-schema.grit" // Validate on the server with the same schema
      ]
    },
    {
      "includes": [
        "app/**/*.{tsx,jsx}",
        "src/**/*.{tsx,jsx}",
        // validate-on-the-server-with-the-same-schema — A rendered example has no route action to post to, so it validates client-side by definition.
        // gate-last-result-on-idle-navigation — A rendered example has no route action and no navigation to be idle, so there is nothing to gate.
        "!**/*.stories.{tsx,jsx}",
        // validate-on-the-server-with-the-same-schema — A rendered example has no route action to post to, so it validates client-side by definition.
        // gate-last-result-on-idle-navigation — A rendered example has no route action and no navigation to be idle, so there is nothing to gate.
        "!**/*.examples.{tsx,jsx}"
      ],
      "plugins": [
        "./ui-lib-rules/gate-last-result-on-idle-navigation.grit" // Gate lastResult on an idle navigation
      ]
    },
    {
      "includes": [
        // no-raw-interactive-elements — Only <input>, and only there.
        // no-appearance-classes-on-layout-elements — Components are made of appearance-styled divs — that is what a component is.
        // no-hardcoded-design-values — The components resolve palette scales deliberately — a danger state on red-500, a Badge intent on emerald-500 — and they are not yours to re-token.
        // import-components-not-primitives — This is the layer that imports the primitives — it is what makes the components components.
        // keep-files-readable — You did not write it and cannot split it without forking it.
        // format-values-through-the-library — The formatters this rule points at are implemented there: FormattedNumber, FormattedDate and the Calendar's month/year labels format through Intl themselves — via the cached constructors in `@/lib/intl`, which build one formatter per (locale, options) pair instead of one per rendered value — which is what makes the rest of the codebase able not to.
        // bind-fields-through-conform — The conform-* variants are where the metadata is read off the field and put onto a control.
        // render-field-text-through-the-field — The conform-* variants and the field primitives are where this markup is supposed to live — they are the layer that renders the label, the description, and the error, and wires the ids between them.
        // validate-on-the-server-with-the-same-schema — A ui-lib form is a control surface, not a submission.
        // seed-toggles-with-default-selected — That is the layer that reads the metadata off the field and names it onto a control.
        "components/ui/**",
        // no-raw-interactive-elements — Only <input>, and only there.
        // no-appearance-classes-on-layout-elements — Components are made of appearance-styled divs — that is what a component is.
        // no-hardcoded-design-values — The components resolve palette scales deliberately — a danger state on red-500, a Badge intent on emerald-500 — and they are not yours to re-token.
        // import-components-not-primitives — This is the layer that imports the primitives — it is what makes the components components.
        // keep-files-readable — You did not write it and cannot split it without forking it.
        // format-values-through-the-library — The formatters this rule points at are implemented there: FormattedNumber, FormattedDate and the Calendar's month/year labels format through Intl themselves — via the cached constructors in `@/lib/intl`, which build one formatter per (locale, options) pair instead of one per rendered value — which is what makes the rest of the codebase able not to.
        // bind-fields-through-conform — The conform-* variants are where the metadata is read off the field and put onto a control.
        // render-field-text-through-the-field — The conform-* variants and the field primitives are where this markup is supposed to live — they are the layer that renders the label, the description, and the error, and wires the ids between them.
        // validate-on-the-server-with-the-same-schema — A ui-lib form is a control surface, not a submission.
        // seed-toggles-with-default-selected — That is the layer that reads the metadata off the field and names it onto a control.
        "src/components/**"
      ],
      "linter": {
        "rules": {
          "correctness": {
            "noRestrictedElements": {
              "level": "error",
              "options": {
                "elements": {
                  "button": "Use <Button> from @/components/button (performs an action in place), or <LinkButton> from @/components/link-button (navigates — a control that changes the URL must be an anchor), or <Toggle> from @/components/toggle (has an on/off pressed state).",
                  "a": "Use <Link> from @/components/link (an inline text link (it renders a plain anchor for http(s)/mailto/tel hrefs, so external links keep working outside a router)), or <LinkButton> from @/components/link-button (a link that should look like a button).",
                  "select": "Use <Select> from @/components/select (a single choice from a known list), or <ComboBox> from @/components/combo-box (the list is long enough to need typeahead), or <MultipleSelect> from @/components/multiple-select (multiple).",
                  "textarea": "Use <Textarea> from @/components/textarea.",
                  "form": "Use <Form> from react-router (submitting to a route action), or <ConformField> from @/components/conform-field (binding fields to a Conform form).",
                  "label": "Use <Label> from @/components/field (composing a field by hand).",
                  "dialog": "Use <Modal> from @/components/modal (a centered overlay), or <Drawer> from @/components/drawer (an edge-anchored panel), or <Sheet> from @/components/sheet (a side sheet), or <Dialog> from @/components/dialog (the dialog content itself (title, body, footer)).",
                  "table": "Use <Table> from @/components/table (a static table (with TableHeader, TableBody, TableColumn, TableRow, TableCell)), or <DataTable> from @/components/data-table (the rows you have are all of them — the browser holds the dataset and sorts, filters, pages and groups it), or <ServerTable> from @/components/server-table (the rows are a page of a larger answer — sorting, filtering and paging are queries the server runs), or <TableShell> from @/components/table-shell (neither mode fits and you are bringing a TanStack instance of your own)."
                }
              }
            }
          },
          "style": {
            "noRestrictedImports": "off",
            "noExcessiveLinesPerFile": "off"
          }
        }
      }
    }
  ]
}

15 rules

Every rule names what to import instead of what it forbids, shows a real wrong/right pair, and lists the exceptions this config applies.

back to the rules →