Product grids that everyone can shop: cards, Load more, quick view and add to cart

How to make product grids and collection pages on a small business online store work with a keyboard, screen readers and phones: one-link product cards, page numbers vs Load more vs infinite scroll, quick view dialogs, add-to-cart messages, sold-out items, platform notes and a 5-minute check.

Most online shopping happens on a grid: a collection page of gift ideas, a "New in" page, a sale page or a list of search results. Each product card holds a photo, a name, a price, maybe a "Quick view" button, an "Add to cart" button or a "Sold out" label, and the grid ends with page numbers, a "Load more" button or an endless scroll. Shoppers this season are also openly abandoning shopping apps that feel like a casino, with spinning wheels, animations and constant pop-ups, and flash sales that sell out mid-checkout leave people frustrated. A calm, predictable grid that works with a keyboard, a screen reader and a phone is a quiet advantage for a small store. Here's how to fix the common problems.

1. Product cards: one clear link, a real price and a useful image

A common theme pattern wraps the photo in one link and the name in another, so a screen reader hears every product twice and keyboard users press Tab twice per card. Use one link with the product name as its text (2.4.4 Link Purpose (In Context)), and give the photo empty alt text when the name right next to it already says what it is, or short alt text that adds something the name doesn't (1.1.1 Non-text Content). Show the price, sale price and stock as text, not only as colour or a badge image (1.3.1 Info and Relationships).

<ul class="product-grid" id="products">
  <li class="product-card">
    <img src="/images/soy-candle-cedar.jpg" alt="" width="400" height="400">
    <h3><a href="/products/soy-candle-cedar" class="card-link">Soy candle, cedar, 8 oz</a></h3>
    <p><span class="visually-hidden">Price: </span>$24.00</p>
    <p>In stock</p>
  </li>
  <li class="product-card">
    <img src="/images/host-gift-set.jpg" alt="" width="400" height="400">
    <h3><a href="/products/host-gift-set" class="card-link">Host gift set</a></h3>
    <p><s><span class="visually-hidden">Was </span>$45.00</s> <span class="visually-hidden">Now </span>$38.00</p>
    <p>Low stock: 3 left</p>
  </li>
</ul>

<style>
  /* makes the whole card clickable while keeping one link in the code */
  .product-card { position: relative; }
  .card-link::after { content: ""; position: absolute; inset: 0; }
  .card-link:focus-visible { outline: none; }
  .product-card:focus-within { outline: 3px solid #1a1a1a; outline-offset: 2px; }
  .visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
</style>
<!-- example products, prices and stock; use your real ones -->

The products, prices and stock numbers above are example data. Only show a low-stock count if it is real and current. If you put other buttons inside the card (such as Quick view or Add to cart), give them position: relative; z-index: 1 so they stay clickable above the card link. For sale prices and stock labels, see our guide to colour-only meaning, sale prices and stock status.

2. Page numbers, "Load more" or infinite scroll

Plain page numbers are the easiest for everyone: each page has its own address, people can bookmark or share it, and the footer is always reachable. Put them in a named <nav> and mark the current page with aria-current="page". Infinite scroll, where products keep appearing as you scroll, is the hardest: keyboard users can never reach the footer with your contact details, help links and returns policy (3.2.6 Consistent Help asks that help be found in the same place on each page), screen reader users lose their place, and phone users can't find where they were after opening a product. If you like the feel of endless loading, use a "Load more" button instead: it moves focus to the first new product and announces the new count (2.4.3 Focus Order, 4.1.3 Status Messages).

<nav aria-label="Product pages">
  <ul class="pagination">
    <li><a href="/collections/gifts?page=1">1</a></li>
    <li><a href="/collections/gifts?page=2" aria-current="page">2</a></li>
    <li><a href="/collections/gifts?page=3">3</a></li>
    <li><a href="/collections/gifts?page=3">Next page</a></li>
  </ul>
</nav>

<!-- or, a "Load more" button that keeps focus in the right place -->
<p id="grid-status" role="status" class="visually-hidden"></p>
<button type="button" id="load-more" data-next="2">Load more products</button>

<script>
  // example: your platform's URL for "the next batch of product cards" will differ
  var TOTAL = 48;
  var list = document.getElementById('products');
  var btn = document.getElementById('load-more');
  var status = document.getElementById('grid-status');

  btn.addEventListener('click', function () {
    var next = Number(btn.dataset.next);
    var before = list.children.length;
    btn.textContent = 'Loading...';
    fetch('/collections/gifts?page=' + next + '&view=cards')
      .then(function (r) { return r.text(); })
      .then(function (html) {
        list.insertAdjacentHTML('beforeend', html);
        var first = list.children[before];
        if (first) first.querySelector('a').focus();
        status.textContent = 'Showing ' + list.children.length + ' of ' + TOTAL + ' products';
        btn.dataset.next = next + 1;
        btn.textContent = 'Load more products';
        if (list.children.length >= TOTAL) btn.hidden = true;
      })
      .catch(function () {
        btn.textContent = 'Load more products';
        status.textContent = 'More products could not be loaded. Please try again.';
      });
  });
</script>
<!-- example URLs, product total and wording -->

The collection URL, page numbers and the product total above are example details. Keep the page-number links in the code as a fallback, so the grid still works if the script fails. Make each page link and the button at least 24 by 24 CSS pixels (2.5.8 Target Size (Minimum)); 44 pixels is kinder on phones.

3. "Quick view" pop-ups

Quick view opens a product's details over the grid. If it's built as a styled <div>, keyboard focus often stays behind it, Escape does nothing and screen readers keep reading the grid underneath. The native HTML <dialog> element opened with showModal() handles most of this for you: it keeps focus inside, closes with Escape and hides the page behind from screen readers. Give the button a name that says which product it opens, send focus back to that button when the dialog closes, and follow the W3C modal dialog pattern (4.1.2 Name, Role, Value, 2.1.1 Keyboard).

<button type="button" class="qv-open" data-product="soy-candle-cedar">
  Quick view<span class="visually-hidden">: Soy candle, cedar</span>
</button>

<dialog id="quick-view" aria-labelledby="qv-title">
  <h2 id="qv-title">Soy candle, cedar, 8 oz</h2>
  <p>$24.00. In stock. Burns for about 40 hours.</p>
  <a href="/products/soy-candle-cedar">See full details</a>
  <button type="button" id="qv-close">Close</button>
</dialog>

<script>
  var dlg = document.getElementById('quick-view'), opener = null;
  document.querySelectorAll('.qv-open').forEach(function (b) {
    b.addEventListener('click', function () {
      opener = b;
      // fill the dialog with the details for b.dataset.product here
      dlg.showModal();
    });
  });
  document.getElementById('qv-close').addEventListener('click', function () { dlg.close(); });
  dlg.addEventListener('close', function () { if (opener) opener.focus(); });
</script>
<!-- example product details and wording -->

The product details above are example data. The visible button text "Quick view" comes first in its name, so voice control users can still say "click Quick view". If your theme's quick view can't be fixed, it's fine to turn it off: a link to the product page does the same job and is simpler for everyone.

4. "Add to cart" from the grid, and sold-out items

A grid of identical "Add to cart" buttons sounds like a list of the same words to a screen reader, so add the product name in hidden text. After adding, say so in a status message, without moving focus or opening a cart drawer that covers the page (4.1.3 Status Messages). For sold-out items, don't hide the product or show a greyed-out button that keyboard users can't reach; say "Sold out" in text and, if you offer one, link a "Tell me when it's back" form with a labelled email field.

<form action="/cart/add" method="post" class="card-add">
  <input type="hidden" name="id" value="soy-candle-cedar">
  <button type="submit">Add to cart<span class="visually-hidden">: Soy candle, cedar</span></button>
</form>

<!-- one message area for the whole page -->
<p id="cart-status" role="status"></p>
<!-- after a successful add, your script sets:
     cart-status text = "Soy candle, cedar added to cart. Your cart has 2 items." -->

<!-- a sold-out card -->
<p>Sold out</p>
<a href="/products/soy-candle-vanilla#back-in-stock">Tell me when Soy candle, vanilla is back</a>
<!-- example product names, IDs and form URL -->

The product names, IDs and URLs above are example details. If a slide-out cart drawer opens after adding, treat it like the quick view dialog above: move focus into it, let Escape close it and return focus to the button. Keep any "Tell me when it's back" email form to one labelled field plus a clear note on what you'll send. See accessible forms.

5. Keep the grid calm

Spinning "spin to win" wheels, confetti, auto-rotating product images on hover and badges that flash can make shopping feel like a slot machine and can be impossible to use for people with attention, vestibular or seizure-related conditions. Anything that moves for more than 5 seconds by itself needs a way to pause it (2.2.2 Pause, Stop, Hide). Sticky "Add to cart" bars and chat bubbles must not cover the product a keyboard user has just tabbed to (2.4.11 Focus Not Obscured (Minimum)). See our guides to animation and motion and holiday sale timers and pop-ups.

Platform notes

Settings and app names change often, so check your platform's or app's own help pages for the current option.

Common mistakes

A 5-minute check you can do today

  1. Open your busiest collection or sale page and press Tab through the first three products. How many stops does each card take, and can you always see where you are? See our keyboard test.
  2. Keep pressing Tab. Can you reach the next page or "Load more", and then the footer?
  3. If you have "Load more", press it with Enter. Does focus land on the first new product?
  4. Open a quick view with the keyboard, press Escape, and check focus returns to the same product.
  5. Add a product to the cart from the grid. Is there a message saying it worked, and does anything cover the page?
  6. With a screen reader (VoiceOver or NVDA), list the links on the page. Does each product appear once, by name? See our screen reader test.

What a scan can and can't check here

Our scan is automated, so it finds only part of the barriers on the pages it visits. On a collection page it can flag images with no alt attribute, icon buttons with no name, empty links, some ARIA mistakes and low contrast. It doesn't press "Load more", open quick views or add to cart, so it can't tell whether focus moves to the right place, whether messages are announced, or whether infinite scroll hides your footer, which is why the 5-minute check above matters. Our Fix-Pack reports include copy-paste fixes for your platform for what the scan finds.

Related: site search and results pages, online store checkout, price filters and sliders, alt text examples, WCAG 2.2 explained.

This is general technical guidance, not legal advice, and it doesn't certify compliance with the ADA or any law, or protect against claims.

Want the fixes written for you?

Start with a free homepage snapshot. The $149 Fix-Pack scans up to 10 pages and gives copy-paste fixes for your platform within 48 hours, with a 14-day refund promise. See a sample report first.

This is general technical guidance, not legal advice, and it does not certify compliance with the ADA or any law.

Writing product pages? See size charts, option pickers, quantity and photo zoom.

Selling products with accessibility features? See accessibility feature tags and filters.

Running a Black Friday or holiday sale? See promo codes, sale prices and sale banners.

Putting together a holiday gift guide? See holiday gift guide pages, gift boxes and bundles.

Adding heart icons or a wishlist? See wishlist, favorite and save-for-later buttons.

Related guides

All guides

This guide is general technical information, not legal advice. Following it does not certify ADA or any legal compliance. Automated checks find only part of accessibility barriers.

Check your own site, then decide

  1. Free homepage snapshot. Your top 3 issues in about a minute. No signup, no card.
  2. $149 Fix-Pack, if you want the full list. Up to 10 pages scanned, issues ranked by impact, copy-paste fixes for your platform, delivered within 48 hours, plus one re-scan after you apply them.

Get my free snapshot Order the Fix-Pack – $149

14-day refund promise. See a sample report first. A technical report, not legal advice; automated checks find only part of accessibility barriers.