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.
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
02Two mechanisms, and the difference decides how a rule's documented exceptions are applied.
- Built-in rules
- Interactive elements correctness/noRestrictedElementsPrimitive imports style/noRestrictedImportsFile length style/noExcessiveLinesPerFileBrowser dialogs suspicious/noAlert
Configured in
biome.jsonc, so their exceptions are ordinaryoverrides— Biome's own path scoping. - GritQL plugins
- Layout elements no-appearance-classes-on-layout-elements.grit
- Design values no-hardcoded-design-values.grit
- Value formatting format-values-through-the-library.grit
- Nested cards no-nested-card.grit
- Field binding bind-fields-through-conform.grit
- Labels and errors render-field-text-through-the-field.grit
- Server validation validate-on-the-server-with-the-same-schema.grit
- Toggle defaults seed-toggles-with-default-selected.grit
- Gating lastResult gate-last-result-on-idle-navigation.grit
- Intent buttons intent-buttons-must-not-be-type-button.grit
- Client sorting no-client-sorting-on-a-server-driven-table.grit
Each plugin is loaded by an
overridesentry of its own, whoseincludesare 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-levelpluginsinstead 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// 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 →