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 ruleIt 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.
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-nameWhat 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|github | Human output, machine output, or GitHub Actions annotations. |
| --fail-on error|warn|never | When to exit with code 1. Default: error. |
| --rules | Print the rules with why and how to fix, then exit. |
| --all | Include rules that are off by default (right to left). |
| --offline | Do not look anything up on the network. |
| --include-library | Also audit components/ui and the Ballmac folder. |
| --url <address> | Also load your running app in a browser (see below). |
| --routes /a,/b | Which routes to load with --url. Default: /. |
Check the running app
npx @ballmac/cli check --url http://localhost:3000 --routes /,/pricingReading 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
{
"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-fileIn CI and in an agent
- run: npx @ballmac/cli check --format github --fail-on errorThe 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-propWarningA 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-statesWarningA 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-altErrorAn 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-nameErrorA 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-interactiveErrorA 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-removedErrorThe 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-labelWarningA 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-tabindexErrortabIndex 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-dialogWarningA 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>a11y/link-no-hrefWarningA 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-colorWarningA 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-decorativeWarningA 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-wideWarningA 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-heightNoteh-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 defaultA 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.