Ballmac UI home

Docs

Quality check

Every item here is measured before it ships. ballmac check holds your own app to the same bar: it reads your source, reports what it is sure about, and tells you how to fix it. An AI agent can run it before it says it is done.

Run it

npx @ballmac/cli check              # audit this project
npx @ballmac/cli check src/app      # one folder
npx @ballmac/cli check --rules      # list every rule

It is read-only, needs no account and no network (the rule list is bundled; it only looks up which of your components have states if you are online), and uses the TypeScript version your project already has. Your own components/ui and Ballmac files are skipped unless you pass --include-library.

Output
ballmac check  ·  41 files

error a11y/control-name ×1  A button or link has an icon but no accessible name
  src/app/toolbar.tsx:18:7
    <button> contains only an icon and has no accessible name.
      18 │ <button onClick={() => setOpen(!open)} className="p-2">
  fix: Add aria-label="What it does" to the control, or include visually hidden text such as <span className="sr-only">Close</span>.
  https://ui.ballmac.com/docs/check#a11y-control-name

What it is, and is not

  • Conservative. A rule reports only when it can see the problem in the code. If a value comes from a variable it cannot follow, it stays quiet. A clean result means none of these specific mistakes was found, not that the app is accessible.
  • Not a replacement for testing. Automated checks find a subset of problems. Use a keyboard and a screen reader on the real app as well.
  • Honest about severity. Errors are things that break for someone (an image with no alt, a button with no name). Warnings are likely gaps. You decide what fails the build.

Flags

--format pretty|json|githubHuman output, machine output, or GitHub Actions annotations.
--fail-on error|warn|neverWhen to exit with code 1. Default: error.
--rulesPrint the rules with why and how to fix, then exit.
--allInclude rules that are off by default (right to left).
--offlineDo not look anything up on the network.
--include-libraryAlso audit components/ui and the Ballmac folder.
--url <address>Also load your running app in a browser (see below).
--routes /a,/bWhich routes to load with --url. Default: /.

Check the running app

npx @ballmac/cli check --url http://localhost:3000 --routes /,/pricing

Reading source cannot see what the browser renders. With --url the tool opens each route with the Playwright and axe-core that are already in your project (it never installs anything), in light and dark at 390 and 1280 pixels wide, and reports serious or critical axe violations, sideways scrolling on a phone, and console errors. If Playwright is not installed it says so and still runs the source checks.

Settings

.ballmacrc.json (or a “ballmac” key in package.json)
{
  "rules": { "layout/fixed-wide": "warn", "tokens/hardcoded-color": "off", "rtl/physical-direction": "warn" },
  "ignore": ["src/legacy/**", "**/*.stories.tsx"],
  "rtl": true
}

Silence one finding where it is, with a reason a teammate can read:

{/* ballmac-ignore-next-line a11y/control-name: the label is added by the wrapper */}
<IconButton onClick={close} />

// ballmac-ignore-line
// ballmac-ignore-file

In CI and in an agent

.github/workflows/quality.yml
- run: npx @ballmac/cli check --format github --fail-on error

The Ballmac MCP server has get_quality_rules, so an agent can read the rules before it writes a component, and audit_project, which runs this check and returns the findings worst first, each with its fix. Tell your agent: “before you finish, run audit_project and fix every error.”

Rules

15 rules. Each one says why it matters to the person using your app and how to fix it.

States

states/ballmac-state-propWarning

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-statesWarning

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-altError

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-nameError

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-interactiveError

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-removedError

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-labelWarning

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-tabindexError

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-dialogWarning

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>

Tokens

tokens/hardcoded-colorWarning

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-decorativeWarning

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-wideWarning

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-heightNote

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-directionOff 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-.

Limits

  • It reads TypeScript and JSX. Other file types and styles written in separate stylesheets are only partly covered (motion in CSS is).
  • Components from other libraries are treated as able to render what they need. Only plain elements and Ballmac components are judged.
  • Contrast is measured only by --url (axe), because it depends on the final rendered colors.

See how the library itself scores on the quality page. Found a false positive? Tell us and include the line.