Accessible Patterns for Jay-HTML

Accessible Patterns for Jay-HTML

Accessible Patterns for Jay-HTML

DesignerDesigner Agent Kit — documentation written for AI agents, readable by humans.

Common accessibility patterns and how to fix validation errors. Each section corresponds to an a11y-validator rule.

Images

Every <img> must have an alt attribute.

<!-- Informative image -->
<img src="product.jpg" alt="Blue running shoes, side view" />

<!-- Decorative image (empty alt) -->
<img src="divider.svg" alt="" />

If the image is the only content of a link or button, the alt text becomes the accessible name.

Form Labels

Every form control needs an accessible name. Three valid approaches:

<!-- 1. Wrapping label (simplest) -->
<label>Email <input type="text" /></label>

<!-- 2. Explicit label with for/id -->
<label for="email">Email</label>
<input type="text" id="email" />

<!-- 3. aria-label (when no visible label) -->
<input type="search" aria-label="Search products" />

One label per control — don't nest multiple inputs in one <label>.

Checkbox and Radio

Same rules apply:

<label><input type="checkbox" /> I agree to the terms</label>

<!-- Or with for/id -->
<input type="radio" name="size" id="size-s" />
<label for="size-s">Small</label>

ARIA Labeling

aria-label must not be empty. aria-labelledby must reference existing id values:

<!-- Valid -->
<span id="search-label">Search</span>
<input type="text" aria-labelledby="search-label" />

<!-- Invalid: empty aria-label -->
<input type="text" aria-label="" />

<!-- Invalid: missing id -->
<input type="text" aria-labelledby="nonexistent" />

Buttons

Buttons need an accessible name — text content, aria-label, or a child <img> with alt:

<button>Add to Cart</button>
<button aria-label="Close dialog"><svg>...</svg></button>
<button><img src="close.svg" alt="Close" /></button>

Nested Interactive Elements

Links and buttons cannot contain other interactive elements. Browsers restructure the DOM and screen readers announce ambiguous controls.

<!-- Invalid: button inside link -->
<a href="/product">
  Product Name
  <button>Add to Cart</button>
</a>

<!-- Invalid: link inside link -->
<a href="/category">
  Category
  <a href="/subcategory">Subcategory</a>
</a>

Pattern 1 — Separate the interactions

Place each interactive element as a sibling:

<div class="product-card">
  <a href="/product">Product Name</a>
  <button>Add to Cart</button>
</div>

For nested links, flatten into a list:

<nav>
  <a href="/category">Category</a>
  <a href="/subcategory">Subcategory</a>
</nav>

Pattern 2 — Stretched link with ::after

Makes the entire card clickable via CSS while the button stays independent:

<article class="product-card">
  <img src="shoe.jpg" alt="Blue running shoe" />
  <h2>
    <a href="/product/123" class="stretched-link">Blue Runner X</a>
  </h2>
  <p>$89.99</p>
  <button type="button">Add to Cart</button>
</article>
.product-card {
  position: relative;
}

.stretched-link::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
}

/* Raise button above the stretched overlay */
button {
  position: relative;
  z-index: 2;
}

The ::after pseudo-element covers the card, making it clickable everywhere — except the button, which sits on a higher z-index. No nested interactive elements, fully accessible.

Duplicate IDs

Each id must be unique in the document. Duplicate IDs break <label for>, aria-labelledby, and anchor links.

<!-- Invalid -->
<label for="name">Name</label>
<input id="name" type="text" />
<input id="name" type="text" />
<!-- duplicate! -->

<!-- Fix: use unique ids -->
<input id="first-name" type="text" />
<input id="last-name" type="text" />

Media Autoplay

Media with autoplay must also have muted:

<video autoplay muted loop>
  <source src="hero.mp4" type="video/mp4" />
</video>

ARIA Roles

Use valid WAI-ARIA roles. Common roles: button, link, navigation, search, dialog, alert, status, tab, tabpanel, menu, menuitem.

<div role="navigation" aria-label="Main">...</div>
<div role="search">...</div>

Non-interactive elements with tabindex="0" must have a role:

<!-- Invalid: focusable but no role -->
<div tabindex="0">Click me</div>

<!-- Fix: add a role -->
<div tabindex="0" role="button">Click me</div>

Tabindex

Avoid positive tabindex — it disrupts natural tab order. Use tabindex="0" to add an element to the tab order, or tabindex="-1" for programmatic focus only.

<!-- Avoid -->
<button tabindex="3">Third</button>

<!-- Prefer: let DOM order determine tab order -->
<button>First</button>
<button>Second</button>

Viewport Zoom

Don't disable user zooming:

<!-- Invalid -->
<meta name="viewport" content="user-scalable=no" />
<meta name="viewport" content="maximum-scale=1" />

<!-- Valid -->
<meta name="viewport" content="width=device-width, initial-scale=1" />

About this document

This page is part of the Jay Stack Agent Kit — documentation generated from the framework source and written primarily for AI agents. The language and structure are optimized for machine consumption — expect precise, specification-style prose rather than narrative documentation. Learn more about the Agent Kit →