Collection Row
Horizontal product shelf with a heading, list semantics, and Prev/Next paging that moves focus to newly revealed items.
Selection
The criteria an agent checks before retrieving this component.
Use when
- Use when displaying multiple related items in a horizontally scrollable row under a shared category heading (e.g., "Customers Also Viewed", "Action Movies").
- Use when multiple items are visible simultaneously and can be scrolled left or right.
- Use when each item is a compact card with a primary visual element and brief supporting text.
Try a different component when
- Do not use when only one item is visible at a time within a rotatable sequence (use
carousel). - Do not use when items are arranged in a multi-row or multi-column layout (use
grid). - Do not use when items are presented as a simple vertical list without horizontal scrolling (use
list).
Must Haves
Non-negotiable structure. Every generated instance must satisfy these rules.
- Use a visible heading, typically an
<h2>, above the row. - Use list semantics for the row:
ulwithliitems. - Each item must comprise a single focus stop: in other words, consist of a single link
<a>that contains:- A visual element (image, poster, thumbnail, or media preview).
- A visible title that identifies the item.
- Optional visible metadata (e.g., price, episode number, rating).
- Each item link must have an accessible name composed of:
- title + metadata (optional) via
aria-labelledby
- title + metadata (optional) via
- Each item link should expose position context (e.g., "3 of 18") as supplemental information:
- Provide an offscreen "X of Y" element.
- Reference it via
aria-describedby. - The position must reflect the item's index within the full set, not just the currently visible subset.
- Provide paging controls:
- Next button on the right edge of the row container (vertically centered)
- Previous button on the left edge when not on the first page
- Paging focus behavior:
- Activating Next moves focus to the first newly revealed item (left-most visible link).
- For example, if items 1 through 6 are visible, and the user activates the Next button, then items 7 through 12 become visible, and focus moves to item 7.
- Activating Previous moves focus to the last newly revealed item (right-most visible link).
- Activating Next moves focus to the first newly revealed item (left-most visible link).
- Ensure a visible focus state (e.g., a 2px solid outline offset by 1-2px) on each item and button.
Donts
Avoid these accessibility and UX barriers.
- Do not let Tab from the last visible item move into out-of-view items; it must reach the Next button instead.
- Do not split the item into multiple separate interactive elements (one item = one link).
- Do not rely solely on poster art or imagery to communicate the name of each item.
Customizable
Alternatives and options that give the AI agent some room to move.
- In the golden pattern, we wrap the component in a container with
role="group"andaria-labelledbypointing to the heading ID. This is optional. Engineers may choose instead to use a<section>orrole="region", or to eschew the container entirely. - Items must at minimum have some "title" text that gives each item a name, but they are not required to also have metadata, like a price, or rating, etc.
Golden Pattern
The tested reference implementation. Agents start from this shape and adapt to the developer’s codebase and context.
"use client";
export function CollectionRow({ heading = "Customers Also Viewed", items = ITEMS, pageSize = 4 }) {
const headingId = useId();
const [startIndex, setStartIndex] = useState(0);
const linkRefs = useRef([]);
const total = items.length;
const endIndex = Math.min(startIndex + pageSize, total);
const visible = items.slice(startIndex, endIndex);
const canPrev = startIndex > 0;
const canNext = endIndex < total;
function goNext() {
if (!canNext) return;
const nextStart = Math.min(startIndex + pageSize, Math.max(total - pageSize, 0));
setStartIndex(nextStart);
requestAnimationFrame(() => linkRefs.current[0]?.focus());
}
function goPrev() {
if (!canPrev) return;
const prevStart = Math.max(startIndex - pageSize, 0);
setStartIndex(prevStart);
requestAnimationFrame(() => linkRefs.current[visible.length - 1]?.focus());
}
return (
<div role="group" aria-labelledby={headingId}>
<h2 id={headingId}>{heading}</h2>
{canPrev && (
<button type="button" onClick={goPrev} aria-label="Previous items">
Prev
</button>
)}
<ul>
{visible.map((item, i) => {
const globalIndex = startIndex + i;
const titleId = `${headingId}-title-${globalIndex}`;
const metaId = `${headingId}-meta-${globalIndex}`;
const posId = `${headingId}-pos-${globalIndex}`;
return (
<li key={item.id}>
<a
href={item.href}
ref={(el) => (linkRefs.current[i] = el)}
aria-labelledby={`${titleId} ${metaId}`}
aria-describedby={posId}
>
<span aria-hidden="true">[image]</span>
<div id={titleId}>{item.title}</div>
<span id={posId} style={srOnly}>
{globalIndex + 1} of {total}
</span>
<div id={metaId}>{item.meta}</div>
</a>
</li>
);
})}
</ul>
{canNext && (
<button type="button" onClick={goNext} aria-label="Next items">
Next
</button>
)}
</div>
);
}
// Visually-hidden styles matching the global sr-only utility (global.sr-only).
const srOnly = {
position: "absolute",
width: 1,
height: 1,
padding: 0,
margin: -1,
overflow: "hidden",
clip: "rect(0,0,0,0)",
whiteSpace: "nowrap",
border: 0,
};
const ITEMS = [
{ id: "1", title: "Item One", meta: "$24.95", href: "#" },
{ id: "2", title: "Item Two", meta: "$29.00", href: "#" },
{ id: "3", title: "Item Three", meta: "$18.50", href: "#" },
{ id: "4", title: "Item Four", meta: "$22.00", href: "#" },
{ id: "5", title: "Item Five", meta: "$27.99", href: "#" },
{ id: "6", title: "Item Six", meta: "$16.95", href: "#" },
{ id: "7", title: "Item Seven", meta: "$25.50", href: "#" },
{ id: "8", title: "Item Eight", meta: "$34.00", href: "#" },
];
Acceptance Checks
The component’s test spec — an optional body of checks for verification.
Structure
- A visible heading is present.
- The row uses
ul/lisemantics. - Each item is a single link wrapping its content.
Accessible naming
- Each link exposes a programmatic name that includes the visible title.
- If metadata is present, it contributes to the accessible name.
- Each link exposes position context (e.g., "3 of 18") once via
aria-describedby.
Keyboard
- Tab order reaches Previous and Next buttons without forcing navigation through hidden items.
- Activating Next moves focus to the first newly visible item.
- Activating Previous moves focus to the last newly visible item.
- Tabbing from the last visible item moves to the Next button (not to hidden items).
Visual focus
- All interactive elements (item links and paging buttons) have a visible focus indicator.