Ballmac UI home

Check rules

Every rule ballmac check and the audit_project tool apply, with why it matters, how to fix it and an example.

On this page

This page is generated from packages/audit/src/rules/meta.ts. To change it, change the source and run pnpm docs:generate.

15 rules. Each one says why it matters to the person using your app and how to fix it. To run them, see Check your own app. To change a rule's level, set it in .ballmacrc.json.

States

states/ballmac-state-prop

Warning. A Ballmac component with states is used without a state prop

The component can show a loading skeleton, an empty message and an error with a retry, but this use only ever shows the ready view. When the request is slow or fails, people see a blank area or a frozen screen.

Fix: Pass state from your data hook, for example state={isPending ? "loading" : isError ? "error" : rows.length ? "ready" : "empty"} and onRetry={refetch}.

Instead of:

<DashboardPro1 />

Write:

<DashboardPro1 state={isPending ? "loading" : isError ? "error" : "ready"} onRetry={refetch} />

states/data-without-states

Warning. A component fetches data but never reads loading or error state

While the request is running, or if it fails, the screen has nothing to show. That is the first thing a person sees on a slow connection.

Fix: Read the loading and error values the hook returns (isPending, isError, error), render a skeleton for loading and a message with a retry for errors, and an empty state when there is no data.

Instead of:

const { data } = useQuery(...)
return <List items={data} />

Write:

const { data, isPending, isError, refetch } = useQuery(...)
if (isPending) return <ListSkeleton />
if (isError) return <ErrorState onRetry={refetch} />
return <List items={data} />

Accessibility

a11y/img-alt

Error. An image has no alt attribute

Screen reader users hear the file name or nothing. Search engines and failed loads show nothing useful either.

Fix: Add alt with a short description of what the image shows, or alt="" if it is purely decorative.

Instead of:

<img src="/hero.png" />

Write:

<img src="/hero.png" alt="The dashboard with a revenue chart" />

a11y/control-name

Error. A button or link has an icon but no accessible name

Screen reader users hear only “button” with no hint of what it does, and voice control users cannot say its name.

Fix: Add aria-label="What it does" to the control, or include visually hidden text such as <span className="sr-only">Close</span>.

Instead of:

<button onClick={close}><XIcon /></button>

Write:

<button onClick={close} aria-label="Close"><XIcon aria-hidden="true" /></button>

a11y/clickable-non-interactive

Error. A click handler is on an element that keyboards cannot reach

A <div> with onClick cannot be focused or activated with the keyboard, and screen readers do not announce it as something to press.

Fix: Use a <button> (or <a> for navigation). If it truly must stay a div, add role, tabIndex={0} and onKeyDown for Enter and Space.

Instead of:

<div onClick={open}>Open</div>

Write:

<button type="button" onClick={open}>Open</button>

a11y/focus-outline-removed

Error. The focus outline is removed and nothing replaces it

Keyboard users lose track of where they are. It is one of the most common ways an app becomes unusable without a mouse.

Fix: Keep the default outline, or replace it with a visible style such as focus-visible:ring-2 focus-visible:ring-ring.

Instead of:

<button className="outline-none">

Write:

<button className="outline-none focus-visible:ring-2 focus-visible:ring-ring">

a11y/input-label

Warning. A form field has no label

A placeholder disappears as soon as someone types and is not a label. Screen readers announce an unnamed field, and clicking the text does not focus it.

Fix: Add <label htmlFor="id"> that matches the field's id, wrap the field in a <label>, or add aria-label.

Instead of:

<input placeholder="Email" />

Write:

<label htmlFor="email">Email</label>
<input id="email" />

a11y/positive-tabindex

Error. tabIndex is greater than zero

It pulls the element to the front of the tab order, so focus jumps around the page in a way nobody expects.

Fix: Use tabIndex={0} to make an element focusable in its natural place, or tabIndex={-1} to focus it from code only.

a11y/hand-rolled-dialog

Warning. A full-screen overlay is built by hand

A hand-made modal usually misses the focus trap, Escape to close, scroll lock and the dialog role, so keyboard and screen reader users get stuck behind it.

Fix: Use the Dialog, Sheet or Drawer component (Radix-based), which does all of that.

Instead of:

<div className="fixed inset-0 z-50 bg-black/50">…</div>

Write:

<Dialog><DialogContent>…</DialogContent></Dialog>

Warning. A link has no href

An <a> without href is not focusable or announced as a link. People cannot open it in a new tab or copy its address.

Fix: Add an href, or use a <button> if it performs an action instead of navigating.

Tokens

tokens/hardcoded-color

Warning. A fixed colour is used instead of a theme token

Fixed colours do not follow the theme, so dark mode and any re-theming break: white panels glowing in the dark, grey text that cannot be read.

Fix: Use token classes: bg-background, bg-card, text-foreground, text-muted-foreground, border-border, bg-primary, text-destructive.

Instead of:

className="bg-white text-gray-900 border-gray-200"

Write:

className="bg-background text-foreground border-border"

Motion

motion/endless-decorative

Warning. A decorative animation runs forever and ignores reduced motion

People with vestibular disorders turn on “reduce motion” because endless movement makes them ill. An animation that ignores the setting is an accessibility bug.

Fix: Add motion-reduce:animate-none to the class, wrap the Motion loop in useReducedMotion, or put the CSS animation inside @media (prefers-reduced-motion: no-preference).

Instead of:

<span className="animate-ping" />

Write:

<span className="animate-ping motion-reduce:animate-none" />

Layout

layout/fixed-wide

Warning. A fixed width is wider than a small phone

A phone is 360 to 390 pixels wide. A fixed 480px element makes the whole page scroll sideways.

Fix: Add max-w-full, or make the width responsive: w-full md:w-[480px].

Instead of:

<div className="w-[480px]">

Write:

<div className="w-full max-w-[480px]">

layout/viewport-height

Note. h-screen is used for a full-height layout

On phones, 100vh includes the area behind the browser's address bar, so the bottom of the page is cut off.

Fix: Use min-h-dvh or h-dvh, which follow the visible area.

Right to left

rtl/physical-direction

Off by default. A left or right class will not mirror in right-to-left languages

In Arabic, Hebrew, Persian and Urdu the layout should be mirrored. Fixed left and right spacing leaves margins and icons on the wrong side.

Fix: Use logical classes: ms-/me- instead of ml-/mr-, ps-/pe- instead of pl-/pr-, text-start/text-end, start-/end- instead of left-/right-.