Rule
prefer-design-system-component
Use the design-system component instead of the native element it wraps (<Button> over <button>).
- What it catches
- Native elements a component is named for (
Buttonfor<button>,Link,Input/TextField,Label,Select,Textarea,Image,Separator,Dialog,Table), or that a component renders and replaces without needing anything the element does not (no required props of its own, the element's attributes). It leaves alone what is not UI to restyle: an element that ishidden,aria-hidden, or invisible and out of the tab order; theasChildchild of a component with classes of its own; a bare<button />,<a />or<label />passed to render into; an element styled by acva/tvdefinition in its own file; and an<input>whose every attribute is spread in - Why it matters
- A native
<button>skips what the design system'sButtoncarries: variants, focus styles, disabled states, and every fix the team made since. Agents write native elements with hand-rolled classes because that is what most code they learned from looks like.
Example
What check_ui returned for this code against the demo design system when the site was built, from version 0.6.0.
<form className="flex gap-2"> <input type="email" placeholder="you@example.com" /> <button className="rounded-md bg-primary px-3 text-sm text-primary-foreground"> Invite </button></form><form className="flex gap-2"> <Input type="email" placeholder="you@example.com" /> <Button className="rounded-md bg-primary px-3 text-sm text-primary-foreground"> Invite </Button></form>error2:4fix included
Native <input> where the design system has <Input>. Use <Input> (import { Input } from "@/components/ui/input").
error3:4fix included
Native <button> where the design system has <Button>. Use <Button> (import { Button } from "@/components/ui/button"); its variants replace the custom classes.
How the fix is chosen
The root component and its import, preferring one that renders the element (NativeSelect over Select); the rename is auto-fixed when it renders that element, takes its attributes or is mapped in elements.
Every finding in the example carries its fix as text edits, which the agent applies, and so does eslint --fix. The code after the fix is those edits, applied.
Options
Its default severity is "error". Set it to "off", "warn" or "error" in onsystem.config.json, or as onsystem/prefer-design-system-component in the ESLint plugin.
Native elements to accept anyway: ["error", { "allow": ["a"] }].
{ "rules": { "prefer-design-system-component": "warn" }}The config file covers the rest, including overrides for folders.
Suppressing a finding
When a finding is intended, a comment turns the rule off for the next line and says why after --. Checked when the site was built: the comment hides the finding on the line below it, and the others stay.
<form className="flex gap-2"> {/* onsystem-disable-next-line prefer-design-system-component -- why this one is intended */} <input type="email" placeholder="you@example.com" /> <button className="rounded-md bg-primary px-3 text-sm text-primary-foreground"> Invite </button></form>onsystem-disable-next-line covers the next line, onsystem-disable-line its own, and /* onsystem-disable */ the rest of the file (at the top, the whole file). Without rule names they turn every rule off. A misspelled rule name is reported as a suppression warning, with the closest one, and suppresses nothing; syntax errors cannot be suppressed. For whole folders or kinds of files, such as legacy screens or generated code, use overrides in the config.