Skip to content
onsystemv0.6.0

Rule

prefer-design-system-component

error by default

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 (Button for <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 is hidden, aria-hidden, or invisible and out of the tab order; the asChild child of a component with classes of its own; a bare <button />, <a /> or <label /> passed to render into; an element styled by a cva/tv definition in its own file; and an <input> whose every attribute is spread in
Why it matters
A native <button> skips what the design system's Button carries: 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.

What the agent wrote
<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>
After the fixthe rule’s own fixes
<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>
check_ui
  1. error2:4fix included

    Native <input> where the design system has <Input>. Use <Input> (import { Input } from "@/components/ui/input").

  2. 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"] }].

onsystem.config.json
{  "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.

With the comment
<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.