Accessibility pattern · Forms and flows

Search filters

Each group of filters is a fieldset with a legend, every option says how many products it would show, and the results change in place while focus stays where you are. The new count is announced once you stop changing filters, applied filters become chips you can remove one at a time, and on a narrow screen the panel folds into a Filters button.

WCAG criteria
7
Keyboard rules
5
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

Shirts and kurtas

Cotton and linen, made in India.

Filter products

Colour
Size
Brand
Price in rupees

Prices here run from ₹849 to ₹2,499.

24 products

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
Tab or ShiftTabMoves through the filters, the sort, the chips and the products. Changing a filter never moves focus.
SpaceOn a filter checkbox, adds or removes that filter; the results update at once.
Arrow Up or Arrow DownIn a price box, steps the price by ₹100; in Sort by, changes the order.
Enter or SpaceOn a chip, removes that filter and moves focus to the next chip; on Clear all, removes every filter.
Enter or SpaceOn a narrow screen, Filters opens or closes the panel; Show results closes it and moves focus to the count.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
Focus reaches a filterColour, group. Navy 4 products, checkbox, not checked
Navy and Rust are ticked in quick succession8 products (once, after the second)
Sort by changesSorted by Price: low to high
Focus reaches a chipRemove Navy, button
The filters leave nothingNo products match these filters. Remove one to see more.
The panel button on a narrow screenFilters 2, button, collapsed

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<div class="ap-filters" data-ap-filters>
  <div class="ap-filters__top">
    <h3 class="ap-filters__name" id="flt-name">Shirts and kurtas</h3>
    <p class="ap-filters__sub">Cotton and linen, made in India.</p>
  </div>

  <div class="ap-filters__layout">
    <div class="ap-filters__side">
      <button type="button" class="ap-btn ap-filters__toggle" aria-expanded="false" aria-controls="flt-panel" data-ap-toggle>
        <svg class="ap-btn__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 6h16M7 12h10M10 18h4"/></svg>
        <span>Filters</span>
        <span class="ap-filters__badge" data-ap-applied hidden>0</span>
      </button>

      <form class="ap-filters__panel" id="flt-panel" aria-labelledby="flt-panel-name" data-ap-panel>
        <p class="ap-filters__panel-name" id="flt-panel-name">Filter products</p>

        <fieldset class="ap-filters__group">
          <legend><span data-ap-group>Colour</span></legend>
          <ul class="ap-filters__opts">
            <li><label class="ap-filters__opt">
              <input type="checkbox" name="colour" value="white" />
              <span class="ap-filters__swatch" data-swatch="white" aria-hidden="true"></span>
              <span class="ap-filters__opt-name">White</span>
              <span class="ap-filters__n"><span data-ap-n>4</span> <span class="ap-filters__vh" data-ap-word>products</span></span>
            </label></li>
            <li><label class="ap-filters__opt">
              <input type="checkbox" name="colour" value="black" />
              <span class="ap-filters__swatch" data-swatch="black" aria-hidden="true"></span>
              <span class="ap-filters__opt-name">Black</span>
              <span class="ap-filters__n"><span data-ap-n>4</span> <span class="ap-filters__vh" data-ap-word>products</span></span>
            </label></li>
            <li><label class="ap-filters__opt">
              <input type="checkbox" name="colour" value="navy" />
              <span class="ap-filters__swatch" data-swatch="navy" aria-hidden="true"></span>
              <span class="ap-filters__opt-name">Navy</span>
              <span class="ap-filters__n"><span data-ap-n>4</span> <span class="ap-filters__vh" data-ap-word>products</span></span>
            </label></li>
            <li><label class="ap-filters__opt">
              <input type="checkbox" name="colour" value="olive" />
              <span class="ap-filters__swatch" data-swatch="olive" aria-hidden="true"></span>
              <span class="ap-filters__opt-name">Olive</span>
              <span class="ap-filters__n"><span data-ap-n>4</span> <span class="ap-filters__vh" data-ap-word>products</span></span>
            </label></li>
            <li><label class="ap-filters__opt">
              <input type="checkbox" name="colour" value="rust" />
              <span class="ap-filters__swatch" data-swatch="rust" aria-hidden="true"></span>
              <span class="ap-filters__opt-name">Rust</span>
              <span class="ap-filters__n"><span data-ap-n>4</span> <span class="ap-filters__vh" data-ap-word>products</span></span>
            </label></li>
            <li><label class="ap-filters__opt">
              <input type="checkbox" name="colour" value="sky" />
              <span class="ap-filters__swatch" data-swatch="sky" aria-hidden="true"></span>
              <span class="ap-filters__opt-name">Sky blue</span>
              <span class="ap-filters__n"><span data-ap-n>4</span> <span class="ap-filters__vh" data-ap-word>products</span></span>
            </label></li>
          </ul>
        </fieldset>

        <fieldset class="ap-filters__group" data-ap-prefixed>
          <legend><span data-ap-group>Size</span></legend>
          <ul class="ap-filters__opts ap-filters__opts--sizes">
            <li><label class="ap-filters__opt">
              <input type="checkbox" name="size" value="S" />
              <span class="ap-filters__opt-name">S</span>
              <span class="ap-filters__n"><span data-ap-n>12</span> <span class="ap-filters__vh" data-ap-word>products</span></span>
            </label></li>
            <li><label class="ap-filters__opt">
              <input type="checkbox" name="size" value="M" />
              <span class="ap-filters__opt-name">M</span>
              <span class="ap-filters__n"><span data-ap-n>22</span> <span class="ap-filters__vh" data-ap-word>products</span></span>
            </label></li>
            <li><label class="ap-filters__opt">
              <input type="checkbox" name="size" value="L" />
              <span class="ap-filters__opt-name">L</span>
              <span class="ap-filters__n"><span data-ap-n>23</span> <span class="ap-filters__vh" data-ap-word>products</span></span>
            </label></li>
            <li><label class="ap-filters__opt">
              <input type="checkbox" name="size" value="XL" />
              <span class="ap-filters__opt-name">XL</span>
              <span class="ap-filters__n"><span data-ap-n>18</span> <span class="ap-filters__vh" data-ap-word>products</span></span>
            </label></li>
            <li><label class="ap-filters__opt">
              <input type="checkbox" name="size" value="XXL" />
              <span class="ap-filters__opt-name">XXL</span>
              <span class="ap-filters__n"><span data-ap-n>7</span> <span class="ap-filters__vh" data-ap-word>products</span></span>
            </label></li>
          </ul>
        </fieldset>

        <fieldset class="ap-filters__group">
          <legend><span data-ap-group>Brand</span></legend>
          <ul class="ap-filters__opts">
            <li><label class="ap-filters__opt">
              <input type="checkbox" name="brand" value="loomcraft" />
              <span class="ap-filters__opt-name">Loomcraft</span>
              <span class="ap-filters__n"><span data-ap-n>6</span> <span class="ap-filters__vh" data-ap-word>products</span></span>
            </label></li>
            <li><label class="ap-filters__opt">
              <input type="checkbox" name="brand" value="nila" />
              <span class="ap-filters__opt-name">Nila</span>
              <span class="ap-filters__n"><span data-ap-n>6</span> <span class="ap-filters__vh" data-ap-word>products</span></span>
            </label></li>
            <li><label class="ap-filters__opt">
              <input type="checkbox" name="brand" value="monsoon" />
              <span class="ap-filters__opt-name">Monsoon Mill</span>
              <span class="ap-filters__n"><span data-ap-n>6</span> <span class="ap-filters__vh" data-ap-word>products</span></span>
            </label></li>
            <li><label class="ap-filters__opt">
              <input type="checkbox" name="brand" value="banyan" />
              <span class="ap-filters__opt-name">Banyan</span>
              <span class="ap-filters__n"><span data-ap-n>6</span> <span class="ap-filters__vh" data-ap-word>products</span></span>
            </label></li>
          </ul>
        </fieldset>

        <fieldset class="ap-filters__group" data-ap-prefixed>
          <legend><span data-ap-group>Price</span> <span class="ap-filters__unit">in rupees</span></legend>
          <p class="ap-hint ap-filters__hint" id="flt-price-hint">Prices here run from ₹849 to ₹2,499.</p>
          <div class="ap-filters__range">
            <div class="ap-field">
              <label class="ap-label" for="flt-min">From</label>
              <input class="ap-input" id="flt-min" name="min" type="number" inputmode="numeric" min="0" step="100" aria-describedby="flt-price-hint" />
            </div>
            <span class="ap-filters__dash" aria-hidden="true">–</span>
            <div class="ap-field">
              <label class="ap-label" for="flt-max">To</label>
              <input class="ap-input" id="flt-max" name="max" type="number" inputmode="numeric" min="0" step="100" aria-describedby="flt-price-hint" />
            </div>
          </div>
          <p class="ap-error ap-filters__error" id="flt-price-error" hidden>
            <svg class="ap-filters__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M12 7.5v5"/><path d="M12 16.5h.01"/></svg>
            <span>The From price must be lower than the To price.</span>
          </p>
        </fieldset>

        <button type="button" class="ap-btn ap-btn--primary ap-filters__done" data-ap-done>Show results</button>
      </form>
    </div>

    <div class="ap-filters__results">
      <div class="ap-filters__bar">
        <h4 class="ap-filters__count" tabindex="-1" data-ap-count><span data-ap-total>24</span> <span data-ap-total-word>products</span></h4>
        <div class="ap-filters__sort">
          <label class="ap-label" for="flt-sort">Sort by</label>
          <select class="ap-input" id="flt-sort" data-ap-sort>
            <option value="featured">Featured</option>
            <option value="low">Price: low to high</option>
            <option value="high">Price: high to low</option>
            <option value="new">Newest first</option>
          </select>
        </div>
      </div>

      <div class="ap-filters__applied" data-ap-chips hidden>
        <p class="ap-filters__applied-name" id="flt-applied-name">Applied filters</p>
        <ul class="ap-filters__chips" aria-labelledby="flt-applied-name" data-ap-chip-list></ul>
        <button type="button" class="ap-filters__clear" data-ap-clear-all>Clear all</button>
      </div>

      <p class="ap-filters__vh" role="status" data-ap-say></p>

      <ul class="ap-filters__grid" aria-labelledby="flt-name" data-ap-grid>
      <li class="ap-filters__card" data-ap-product data-colour="white" data-brand="loomcraft" data-sizes="S M L XL" data-price="1499" data-rank="9">
        <span class="ap-filters__tile" aria-hidden="true"><svg class="ap-filters__garment" viewBox="0 0 48 44" focusable="false" data-swatch="white"><path d="M17 5 9 8 4 17l6 3 3-4v24h22V16l3 4 6-3-5-9-8-3c-1 3-4 5-7 5s-6-2-7-5Z"/></svg></span>
        <a class="ap-filters__product" href="#flt-product-1">White linen shirt</a>
        <span class="ap-filters__maker">Loomcraft</span>
        <span class="ap-filters__price">₹1,499</span>
      </li>
      <li class="ap-filters__card" data-ap-product data-colour="navy" data-brand="nila" data-sizes="M L XL XXL" data-price="1299" data-rank="14">
        <span class="ap-filters__tile" aria-hidden="true"><svg class="ap-filters__garment" viewBox="0 0 48 44" focusable="false" data-swatch="navy"><path d="M17 5 9 8 4 17l6 3 3-4v24h22V16l3 4 6-3-5-9-8-3c-1 3-4 5-7 5s-6-2-7-5Z"/></svg></span>
        <a class="ap-filters__product" href="#flt-product-2">Navy oxford shirt</a>
        <span class="ap-filters__maker">Nila</span>
        <span class="ap-filters__price">₹1,299</span>
      </li>
      <li class="ap-filters__card" data-ap-product data-colour="olive" data-brand="monsoon" data-sizes="S M L" data-price="1199" data-rank="3">
        <span class="ap-filters__tile" aria-hidden="true"><svg class="ap-filters__garment" viewBox="0 0 48 44" focusable="false" data-swatch="olive"><path d="M17 5 9 8 4 17l6 3 3-4v24h22V16l3 4 6-3-5-9-8-3c-1 3-4 5-7 5s-6-2-7-5Z"/></svg></span>
        <a class="ap-filters__product" href="#flt-product-3">Olive mandarin-collar shirt</a>
        <span class="ap-filters__maker">Monsoon Mill</span>
        <span class="ap-filters__price">₹1,199</span>
      </li>
      <li class="ap-filters__card" data-ap-product data-colour="rust" data-brand="banyan" data-sizes="M L XL" data-price="999" data-rank="18">
        <span class="ap-filters__tile" aria-hidden="true"><svg class="ap-filters__garment" viewBox="0 0 48 44" focusable="false" data-swatch="rust"><path d="M17 5 9 8 4 17l6 3 3-4v24h22V16l3 4 6-3-5-9-8-3c-1 3-4 5-7 5s-6-2-7-5Z"/></svg></span>
        <a class="ap-filters__product" href="#flt-product-4">Rust camp-collar shirt</a>
        <span class="ap-filters__maker">Banyan</span>
        <span class="ap-filters__price">₹999</span>
      </li>
      <li class="ap-filters__card" data-ap-product data-colour="sky" data-brand="nila" data-sizes="S M L XL XXL" data-price="1549" data-rank="1">
        <span class="ap-filters__tile" aria-hidden="true"><svg class="ap-filters__garment" viewBox="0 0 48 44" focusable="false" data-swatch="sky"><path d="M17 5 9 8 4 17l6 3 3-4v24h22V16l3 4 6-3-5-9-8-3c-1 3-4 5-7 5s-6-2-7-5Z"/></svg></span>
        <a class="ap-filters__product" href="#flt-product-5">Sky blue linen shirt</a>
        <span class="ap-filters__maker">Nila</span>
        <span class="ap-filters__price">₹1,549</span>
      </li>
      <li class="ap-filters__card" data-ap-product data-colour="black" data-brand="loomcraft" data-sizes="S M L XL" data-price="899" data-rank="22">
        <span class="ap-filters__tile" aria-hidden="true"><svg class="ap-filters__garment" viewBox="0 0 48 44" focusable="false" data-swatch="black"><path d="M18 4 11 7 6 17l6 3 2-4v26h20V16l2 4 6-3-5-10-7-3c-1 3-3 5-6 5s-5-2-6-5Z"/></svg></span>
        <a class="ap-filters__product" href="#flt-product-6">Black short kurta</a>
        <span class="ap-filters__maker">Loomcraft</span>
        <span class="ap-filters__price">₹899</span>
      </li>
      <li class="ap-filters__card" data-ap-product data-colour="white" data-brand="monsoon" data-sizes="M L XL" data-price="2299" data-rank="6">
        <span class="ap-filters__tile" aria-hidden="true"><svg class="ap-filters__garment" viewBox="0 0 48 44" focusable="false" data-swatch="white"><path d="M18 4 11 7 6 17l6 3 2-4v26h20V16l2 4 6-3-5-10-7-3c-1 3-3 5-6 5s-5-2-6-5Z"/></svg></span>
        <a class="ap-filters__product" href="#flt-product-7">White chikankari kurta</a>
        <span class="ap-filters__maker">Monsoon Mill</span>
        <span class="ap-filters__price">₹2,299</span>
      </li>
      <li class="ap-filters__card" data-ap-product data-colour="navy" data-brand="banyan" data-sizes="L XL XXL" data-price="1599" data-rank="11">
        <span class="ap-filters__tile" aria-hidden="true"><svg class="ap-filters__garment" viewBox="0 0 48 44" focusable="false" data-swatch="navy"><path d="M18 4 11 7 6 17l6 3 2-4v26h20V16l2 4 6-3-5-10-7-3c-1 3-3 5-6 5s-5-2-6-5Z"/></svg></span>
        <a class="ap-filters__product" href="#flt-product-8">Navy long kurta</a>
        <span class="ap-filters__maker">Banyan</span>
        <span class="ap-filters__price">₹1,599</span>
      </li>
      <li class="ap-filters__card" data-ap-product data-colour="olive" data-brand="loomcraft" data-sizes="M L XL" data-price="2499" data-rank="2">
        <span class="ap-filters__tile" aria-hidden="true"><svg class="ap-filters__garment" viewBox="0 0 48 44" focusable="false" data-swatch="olive"><path d="M17 5 10 8 7 16v24h12V18l5 6 5-6v22h12V16l-3-8-7-3-7 9-7-9Z"/></svg></span>
        <a class="ap-filters__product" href="#flt-product-9">Olive Nehru jacket</a>
        <span class="ap-filters__maker">Loomcraft</span>
        <span class="ap-filters__price">₹2,499</span>
      </li>
      <li class="ap-filters__card" data-ap-product data-colour="sky" data-brand="monsoon" data-sizes="S M L" data-price="1349" data-rank="16">
        <span class="ap-filters__tile" aria-hidden="true"><svg class="ap-filters__garment" viewBox="0 0 48 44" focusable="false" data-swatch="sky"><path d="M17 5 9 8 4 17l6 3 3-4v24h22V16l3 4 6-3-5-9-8-3c-1 3-4 5-7 5s-6-2-7-5Z"/></svg></span>
        <a class="ap-filters__product" href="#flt-product-10">Sky blue oxford shirt</a>
        <span class="ap-filters__maker">Monsoon Mill</span>
        <span class="ap-filters__price">₹1,349</span>
      </li>
      <li class="ap-filters__card" data-ap-product data-colour="rust" data-brand="nila" data-sizes="S M L" data-price="949" data-rank="8">
        <span class="ap-filters__tile" aria-hidden="true"><svg class="ap-filters__garment" viewBox="0 0 48 44" focusable="false" data-swatch="rust"><path d="M18 4 11 7 6 17l6 3 2-4v26h20V16l2 4 6-3-5-10-7-3c-1 3-3 5-6 5s-5-2-6-5Z"/></svg></span>
        <a class="ap-filters__product" href="#flt-product-11">Rust short kurta</a>
        <span class="ap-filters__maker">Nila</span>
        <span class="ap-filters__price">₹949</span>
      </li>
      <li class="ap-filters__card" data-ap-product data-colour="black" data-brand="banyan" data-sizes="M L XL XXL" data-price="1449" data-rank="20">
        <span class="ap-filters__tile" aria-hidden="true"><svg class="ap-filters__garment" viewBox="0 0 48 44" focusable="false" data-swatch="black"><path d="M17 5 9 8 4 17l6 3 3-4v24h22V16l3 4 6-3-5-9-8-3c-1 3-4 5-7 5s-6-2-7-5Z"/></svg></span>
        <a class="ap-filters__product" href="#flt-product-12">Black linen shirt</a>
        <span class="ap-filters__maker">Banyan</span>
        <span class="ap-filters__price">₹1,449</span>
      </li>
      <li class="ap-filters__card" data-ap-product data-colour="white" data-brand="loomcraft" data-sizes="S M L XL XXL" data-price="1299" data-rank="24">
        <span class="ap-filters__tile" aria-hidden="true"><svg class="ap-filters__garment" viewBox="0 0 48 44" focusable="false" data-swatch="white"><path d="M17 5 9 8 4 17l6 3 3-4v24h22V16l3 4 6-3-5-9-8-3c-1 3-4 5-7 5s-6-2-7-5Z"/></svg></span>
        <a class="ap-filters__product" href="#flt-product-13">White oxford shirt</a>
        <span class="ap-filters__maker">Loomcraft</span>
        <span class="ap-filters__price">₹1,299</span>
      </li>
      <li class="ap-filters__card" data-ap-product data-colour="navy" data-brand="nila" data-sizes="M L" data-price="1249" data-rank="5">
        <span class="ap-filters__tile" aria-hidden="true"><svg class="ap-filters__garment" viewBox="0 0 48 44" focusable="false" data-swatch="navy"><path d="M17 5 9 8 4 17l6 3 3-4v24h22V16l3 4 6-3-5-9-8-3c-1 3-4 5-7 5s-6-2-7-5Z"/></svg></span>
        <a class="ap-filters__product" href="#flt-product-14">Navy mandarin-collar shirt</a>
        <span class="ap-filters__maker">Nila</span>
        <span class="ap-filters__price">₹1,249</span>
      </li>
      <li class="ap-filters__card" data-ap-product data-colour="olive" data-brand="monsoon" data-sizes="S M L XL" data-price="1049" data-rank="13">
        <span class="ap-filters__tile" aria-hidden="true"><svg class="ap-filters__garment" viewBox="0 0 48 44" focusable="false" data-swatch="olive"><path d="M17 5 9 8 4 17l6 3 3-4v24h22V16l3 4 6-3-5-9-8-3c-1 3-4 5-7 5s-6-2-7-5Z"/></svg></span>
        <a class="ap-filters__product" href="#flt-product-15">Olive camp-collar shirt</a>
        <span class="ap-filters__maker">Monsoon Mill</span>
        <span class="ap-filters__price">₹1,049</span>
      </li>
      <li class="ap-filters__card" data-ap-product data-colour="rust" data-brand="banyan" data-sizes="M L XL" data-price="1699" data-rank="4">
        <span class="ap-filters__tile" aria-hidden="true"><svg class="ap-filters__garment" viewBox="0 0 48 44" focusable="false" data-swatch="rust"><path d="M18 4 11 7 6 17l6 3 2-4v26h20V16l2 4 6-3-5-10-7-3c-1 3-3 5-6 5s-5-2-6-5Z"/></svg></span>
        <a class="ap-filters__product" href="#flt-product-16">Rust long kurta</a>
        <span class="ap-filters__maker">Banyan</span>
        <span class="ap-filters__price">₹1,699</span>
      </li>
      <li class="ap-filters__card" data-ap-product data-colour="sky" data-brand="loomcraft" data-sizes="S M L" data-price="2199" data-rank="10">
        <span class="ap-filters__tile" aria-hidden="true"><svg class="ap-filters__garment" viewBox="0 0 48 44" focusable="false" data-swatch="sky"><path d="M18 4 11 7 6 17l6 3 2-4v26h20V16l2 4 6-3-5-10-7-3c-1 3-3 5-6 5s-5-2-6-5Z"/></svg></span>
        <a class="ap-filters__product" href="#flt-product-17">Sky blue chikankari kurta</a>
        <span class="ap-filters__maker">Loomcraft</span>
        <span class="ap-filters__price">₹2,199</span>
      </li>
      <li class="ap-filters__card" data-ap-product data-colour="black" data-brand="nila" data-sizes="L XL XXL" data-price="2399" data-rank="7">
        <span class="ap-filters__tile" aria-hidden="true"><svg class="ap-filters__garment" viewBox="0 0 48 44" focusable="false" data-swatch="black"><path d="M17 5 10 8 7 16v24h12V18l5 6 5-6v22h12V16l-3-8-7-3-7 9-7-9Z"/></svg></span>
        <a class="ap-filters__product" href="#flt-product-18">Black Nehru jacket</a>
        <span class="ap-filters__maker">Nila</span>
        <span class="ap-filters__price">₹2,399</span>
      </li>
      <li class="ap-filters__card" data-ap-product data-colour="white" data-brand="banyan" data-sizes="S M" data-price="949" data-rank="19">
        <span class="ap-filters__tile" aria-hidden="true"><svg class="ap-filters__garment" viewBox="0 0 48 44" focusable="false" data-swatch="white"><path d="M17 5 9 8 4 17l6 3 3-4v24h22V16l3 4 6-3-5-9-8-3c-1 3-4 5-7 5s-6-2-7-5Z"/></svg></span>
        <a class="ap-filters__product" href="#flt-product-19">White camp-collar shirt</a>
        <span class="ap-filters__maker">Banyan</span>
        <span class="ap-filters__price">₹949</span>
      </li>
      <li class="ap-filters__card" data-ap-product data-colour="navy" data-brand="monsoon" data-sizes="M L XL XXL" data-price="849" data-rank="15">
        <span class="ap-filters__tile" aria-hidden="true"><svg class="ap-filters__garment" viewBox="0 0 48 44" focusable="false" data-swatch="navy"><path d="M18 4 11 7 6 17l6 3 2-4v26h20V16l2 4 6-3-5-10-7-3c-1 3-3 5-6 5s-5-2-6-5Z"/></svg></span>
        <a class="ap-filters__product" href="#flt-product-20">Navy short kurta</a>
        <span class="ap-filters__maker">Monsoon Mill</span>
        <span class="ap-filters__price">₹849</span>
      </li>
      <li class="ap-filters__card" data-ap-product data-colour="olive" data-brand="nila" data-sizes="S M L XL" data-price="1649" data-rank="12">
        <span class="ap-filters__tile" aria-hidden="true"><svg class="ap-filters__garment" viewBox="0 0 48 44" focusable="false" data-swatch="olive"><path d="M18 4 11 7 6 17l6 3 2-4v26h20V16l2 4 6-3-5-10-7-3c-1 3-3 5-6 5s-5-2-6-5Z"/></svg></span>
        <a class="ap-filters__product" href="#flt-product-21">Olive long kurta</a>
        <span class="ap-filters__maker">Nila</span>
        <span class="ap-filters__price">₹1,649</span>
      </li>
      <li class="ap-filters__card" data-ap-product data-colour="rust" data-brand="loomcraft" data-sizes="M L XL" data-price="1399" data-rank="21">
        <span class="ap-filters__tile" aria-hidden="true"><svg class="ap-filters__garment" viewBox="0 0 48 44" focusable="false" data-swatch="rust"><path d="M17 5 9 8 4 17l6 3 3-4v24h22V16l3 4 6-3-5-9-8-3c-1 3-4 5-7 5s-6-2-7-5Z"/></svg></span>
        <a class="ap-filters__product" href="#flt-product-22">Rust oxford shirt</a>
        <span class="ap-filters__maker">Loomcraft</span>
        <span class="ap-filters__price">₹1,399</span>
      </li>
      <li class="ap-filters__card" data-ap-product data-colour="sky" data-brand="banyan" data-sizes="S M L XL" data-price="1149" data-rank="17">
        <span class="ap-filters__tile" aria-hidden="true"><svg class="ap-filters__garment" viewBox="0 0 48 44" focusable="false" data-swatch="sky"><path d="M17 5 9 8 4 17l6 3 3-4v24h22V16l3 4 6-3-5-9-8-3c-1 3-4 5-7 5s-6-2-7-5Z"/></svg></span>
        <a class="ap-filters__product" href="#flt-product-23">Sky blue mandarin-collar shirt</a>
        <span class="ap-filters__maker">Banyan</span>
        <span class="ap-filters__price">₹1,149</span>
      </li>
      <li class="ap-filters__card" data-ap-product data-colour="black" data-brand="monsoon" data-sizes="M L XL" data-price="2349" data-rank="23">
        <span class="ap-filters__tile" aria-hidden="true"><svg class="ap-filters__garment" viewBox="0 0 48 44" focusable="false" data-swatch="black"><path d="M18 4 11 7 6 17l6 3 2-4v26h20V16l2 4 6-3-5-10-7-3c-1 3-3 5-6 5s-5-2-6-5Z"/></svg></span>
        <a class="ap-filters__product" href="#flt-product-24">Black chikankari kurta</a>
        <span class="ap-filters__maker">Monsoon Mill</span>
        <span class="ap-filters__price">₹2,349</span>
      </li>
      </ul>
      <p class="ap-filters__empty" data-ap-empty hidden>No products match these filters. Remove one to see more.</p>
    </div>
  </div>
</div>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 1.3.1 Info and Relationships Level A

    Each group is a fieldset with a legend, so a checkbox is announced with its group, and the price boxes have their own labels.

  • 2.4.3 Focus Order Level A

    Filtering never moves focus; removing a chip moves it to the next chip, and clearing everything moves it to the result count.

  • 3.2.2 On Input Level A

    Ticking a filter changes the results in place, not the page or the focus, so it is not a change of context.

  • 3.3.1 Error Identification Level A

    A price range that cannot match anything is named in text beside the boxes, and announced instead of a count.

  • 4.1.2 Name, Role, Value Level A

    The narrow-screen Filters button reports expanded or collapsed and names the panel it controls; each chip is a button named Remove and its filter.

  • 4.1.3 Status Messages Level AA

    The result count is announced from a polite live region, once changes pause, without moving focus.

  • 2.5.8 Target Size (Minimum) Level AA

    Checkbox rows and chips are at least 24 pixels high, with the whole row as the target.

Usage

When to use it

Use it

  • Lists of more than a screenful that people narrow by known attributes: products, listings, jobs, documents.
  • Facets where seeing how many results an option leaves helps people choose.

Use something else

  • Short lists that fit on one screen: filters add work there.
  • Filters that each need a server round trip and a new page: then use a form with an Apply button, so nothing changes until people ask.
  • Free-text questions: a search field answers those better.

Common failures

How it usually goes wrong

  • Moving focus to the results on every change

    Jumping to the grid after each tick means tabbing back to the panel for the next filter. Focus stays put; the count is announced instead.

  • Announcing every change straight away

    Ticking three boxes quickly would read three counts over each other. The announcement waits until changes stop, then says the latest count once.

  • Checkboxes with no group name

    "M, checkbox" tells nobody it is a size. A fieldset and legend add the group, so it is read as Size, M.

  • Chips that say only the filter

    A button named "Navy ×" does not say what pressing it does. Each chip is named Remove Navy.

  • Focus lost when a chip is removed

    The pressed chip disappears, and focus falls to the top of the page. Here it moves to the next chip, or to the result count when none are left.

  • Options that lead to nothing, unannounced

    Without counts, people tick a box and find zero results. Each option shows how many products it would leave.

Notes

Building it

  • Counts are faceted: each option's number is what you would see if you added it to the filters in the other groups. They update with every change.
  • The visible count changes at once; a separate, visually hidden polite region speaks it 700 milliseconds after the last change, and is emptied first so the same number can be heard again.
  • Below 580 pixels of width (a container query, not a viewport one) the panel becomes a disclosure under a Filters button that shows how many filters are applied.
  • Sorting reorders the products in the document, not with CSS order, so the reading and Tab order match what is on screen.
  • Products link to #… anchors in this demo. A real listing also writes the filters into the URL, so a filtered list can be shared and the back button works.

Sources: WAI Tutorials: Grouping controls · Understanding SC 4.1.3: Status Messages · WAI-ARIA Authoring Practices: Disclosure

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction