Accessibility pattern · Data display

Comparison table

A comparison is a data table: plans are column headers, features are row headers, so every tick is announced with the plan and the feature it belongs to. Ticks and crosses carry hidden words, limits stay as visible text, and hiding the rows that match says how many are left.

WCAG criteria
7
Keyboard rules
4
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.

Compare plans

Prices are per month, billed yearly, and include GST.

All 12 features shown

Starter, Pro and Team plans, feature by feature
Feature Starter ₹0 per month Pro Recommended ₹499 per month Team ₹1,499 per month
Projects and files
Projects Up to 3 Unlimited Unlimited
Storage 2 GB 100 GB 1 TB
Version history 7 days 1 year Forever
Export to PDF Included Included Included
Collaboration
Members Just you Up to 5 Unlimited
Comments and mentions Included Included Included
Guest access Not included Included Included
Shared templates Not included Not included Included
Security and support
Two-step sign-in Included Included Included
Single sign-on Not included Not included Included
Audit log Not included 30 days 1 year
Support Help center Email Priority, 4-hour reply
Cancel any time.

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
TabMoves to the switch, then the table's scroll area, then each Choose button.
SpaceOn the switch, hides the features every plan has in common, or shows them all again.
Arrow Down or Arrow Up or Arrow Right or Arrow LeftIn the scroll area, scrolls the table; the plan names stay at the top and the feature names at the left.
Enter or SpaceOn a Choose button, picks that plan and confirms it below the table.

Screen readers

What it announces

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

WhenExpected announcement
Focus reaches the switchShow only differences, switch, off
Space turns it on9 of 12 features shown: only the differences
Table navigation moves right along the Members rowPro, Recommended, ₹499 per month, Up to 5
It reaches a cross in the Guest access rowStarter, ₹0 per month, Not included
Focus reaches the middle buttonChoose Pro, button, ₹499 per month
The button is pressedYou chose Pro. Next: payment details.

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-comparison-table" data-ap-comparison-table>
  <div class="ap-comparison-table__top">
    <div class="ap-comparison-table__intro">
      <h3 class="ap-comparison-table__name" id="comparison-table-name">Compare plans</h3>
      <p class="ap-comparison-table__sub">Prices are per month, billed yearly, and include GST.</p>
    </div>
    <div class="ap-comparison-table__filter">
      <label class="ap-comparison-table__switch">
        <input class="ap-comparison-table__input" type="checkbox" role="switch" data-ap-differences />
        <span class="ap-comparison-table__track" aria-hidden="true"><span class="ap-comparison-table__thumb"></span></span>
        <span class="ap-comparison-table__switch-text">Show only differences</span>
      </label>
      <p class="ap-comparison-table__count" role="status" data-ap-features-shown>All 12 features shown</p>
    </div>
  </div>

  <div class="ap-comparison-table__scroll" role="region" aria-labelledby="comparison-table-name" tabindex="0">
    <table class="ap-comparison-table__table">
      <caption class="ap-comparison-table__vh">Starter, Pro and Team plans, feature by feature</caption>
      <thead>
        <tr>
          <th scope="col" class="ap-comparison-table__corner">Feature</th>
          <th scope="col" class="ap-comparison-table__plan">
            <span class="ap-comparison-table__plan-name">Starter</span>
            <span class="ap-comparison-table__price" id="comparison-table-starter-price"><strong>₹0</strong> per month</span>
          </th>
          <th scope="col" class="ap-comparison-table__plan ap-comparison-table__plan--pick">
            <span class="ap-comparison-table__plan-name">Pro</span>
            <span class="ap-comparison-table__tag">Recommended</span>
            <span class="ap-comparison-table__price" id="comparison-table-pro-price"><strong>₹499</strong> per month</span>
          </th>
          <th scope="col" class="ap-comparison-table__plan">
            <span class="ap-comparison-table__plan-name">Team</span>
            <span class="ap-comparison-table__price" id="comparison-table-team-price"><strong>₹1,499</strong> per month</span>
          </th>
        </tr>
      </thead>
      <tbody>
        <tr class="ap-comparison-table__group">
          <th scope="rowgroup" colspan="4"><span>Projects and files</span></th>
        </tr>
        <tr>
          <th scope="row">Projects</th>
          <td><span class="ap-comparison-table__value">Up to 3</span></td>
          <td><span class="ap-comparison-table__value">Unlimited</span></td>
          <td><span class="ap-comparison-table__value">Unlimited</span></td>
        </tr>
        <tr>
          <th scope="row">Storage</th>
          <td><span class="ap-comparison-table__value">2 GB</span></td>
          <td><span class="ap-comparison-table__value">100 GB</span></td>
          <td><span class="ap-comparison-table__value">1 TB</span></td>
        </tr>
        <tr>
          <th scope="row">Version history</th>
          <td><span class="ap-comparison-table__value">7 days</span></td>
          <td><span class="ap-comparison-table__value">1 year</span></td>
          <td><span class="ap-comparison-table__value">Forever</span></td>
        </tr>
        <tr>
          <th scope="row">Export to PDF</th>
          <td><svg class="ap-comparison-table__mark ap-comparison-table__mark--yes" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="m8 12.4 2.7 2.7L16.2 9.6"/></svg><span class="ap-comparison-table__vh">Included</span></td>
          <td><svg class="ap-comparison-table__mark ap-comparison-table__mark--yes" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="m8 12.4 2.7 2.7L16.2 9.6"/></svg><span class="ap-comparison-table__vh">Included</span></td>
          <td><svg class="ap-comparison-table__mark ap-comparison-table__mark--yes" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="m8 12.4 2.7 2.7L16.2 9.6"/></svg><span class="ap-comparison-table__vh">Included</span></td>
        </tr>
      </tbody>
      <tbody>
        <tr class="ap-comparison-table__group">
          <th scope="rowgroup" colspan="4"><span>Collaboration</span></th>
        </tr>
        <tr>
          <th scope="row">Members</th>
          <td><span class="ap-comparison-table__value">Just you</span></td>
          <td><span class="ap-comparison-table__value">Up to 5</span></td>
          <td><span class="ap-comparison-table__value">Unlimited</span></td>
        </tr>
        <tr>
          <th scope="row">Comments and mentions</th>
          <td><svg class="ap-comparison-table__mark ap-comparison-table__mark--yes" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="m8 12.4 2.7 2.7L16.2 9.6"/></svg><span class="ap-comparison-table__vh">Included</span></td>
          <td><svg class="ap-comparison-table__mark ap-comparison-table__mark--yes" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="m8 12.4 2.7 2.7L16.2 9.6"/></svg><span class="ap-comparison-table__vh">Included</span></td>
          <td><svg class="ap-comparison-table__mark ap-comparison-table__mark--yes" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="m8 12.4 2.7 2.7L16.2 9.6"/></svg><span class="ap-comparison-table__vh">Included</span></td>
        </tr>
        <tr>
          <th scope="row">Guest access</th>
          <td><svg class="ap-comparison-table__mark ap-comparison-table__mark--no" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m7.5 7.5 9 9M16.5 7.5l-9 9"/></svg><span class="ap-comparison-table__vh">Not included</span></td>
          <td><svg class="ap-comparison-table__mark ap-comparison-table__mark--yes" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="m8 12.4 2.7 2.7L16.2 9.6"/></svg><span class="ap-comparison-table__vh">Included</span></td>
          <td><svg class="ap-comparison-table__mark ap-comparison-table__mark--yes" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="m8 12.4 2.7 2.7L16.2 9.6"/></svg><span class="ap-comparison-table__vh">Included</span></td>
        </tr>
        <tr>
          <th scope="row">Shared templates</th>
          <td><svg class="ap-comparison-table__mark ap-comparison-table__mark--no" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m7.5 7.5 9 9M16.5 7.5l-9 9"/></svg><span class="ap-comparison-table__vh">Not included</span></td>
          <td><svg class="ap-comparison-table__mark ap-comparison-table__mark--no" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m7.5 7.5 9 9M16.5 7.5l-9 9"/></svg><span class="ap-comparison-table__vh">Not included</span></td>
          <td><svg class="ap-comparison-table__mark ap-comparison-table__mark--yes" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="m8 12.4 2.7 2.7L16.2 9.6"/></svg><span class="ap-comparison-table__vh">Included</span></td>
        </tr>
      </tbody>
      <tbody>
        <tr class="ap-comparison-table__group">
          <th scope="rowgroup" colspan="4"><span>Security and support</span></th>
        </tr>
        <tr>
          <th scope="row">Two-step sign-in</th>
          <td><svg class="ap-comparison-table__mark ap-comparison-table__mark--yes" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="m8 12.4 2.7 2.7L16.2 9.6"/></svg><span class="ap-comparison-table__vh">Included</span></td>
          <td><svg class="ap-comparison-table__mark ap-comparison-table__mark--yes" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="m8 12.4 2.7 2.7L16.2 9.6"/></svg><span class="ap-comparison-table__vh">Included</span></td>
          <td><svg class="ap-comparison-table__mark ap-comparison-table__mark--yes" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="m8 12.4 2.7 2.7L16.2 9.6"/></svg><span class="ap-comparison-table__vh">Included</span></td>
        </tr>
        <tr>
          <th scope="row">Single sign-on</th>
          <td><svg class="ap-comparison-table__mark ap-comparison-table__mark--no" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m7.5 7.5 9 9M16.5 7.5l-9 9"/></svg><span class="ap-comparison-table__vh">Not included</span></td>
          <td><svg class="ap-comparison-table__mark ap-comparison-table__mark--no" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m7.5 7.5 9 9M16.5 7.5l-9 9"/></svg><span class="ap-comparison-table__vh">Not included</span></td>
          <td><svg class="ap-comparison-table__mark ap-comparison-table__mark--yes" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="m8 12.4 2.7 2.7L16.2 9.6"/></svg><span class="ap-comparison-table__vh">Included</span></td>
        </tr>
        <tr>
          <th scope="row">Audit log</th>
          <td><svg class="ap-comparison-table__mark ap-comparison-table__mark--no" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m7.5 7.5 9 9M16.5 7.5l-9 9"/></svg><span class="ap-comparison-table__vh">Not included</span></td>
          <td><span class="ap-comparison-table__value">30 days</span></td>
          <td><span class="ap-comparison-table__value">1 year</span></td>
        </tr>
        <tr>
          <th scope="row">Support</th>
          <td><span class="ap-comparison-table__value">Help center</span></td>
          <td><span class="ap-comparison-table__value">Email</span></td>
          <td><span class="ap-comparison-table__value">Priority, 4-hour reply</span></td>
        </tr>
      </tbody>
      <tfoot>
        <tr>
          <td class="ap-comparison-table__note">Cancel any time.</td>
          <td><button type="button" class="ap-btn ap-comparison-table__choose" aria-describedby="comparison-table-starter-price" data-ap-plan="Starter">Choose Starter</button></td>
          <td><button type="button" class="ap-btn ap-btn--primary ap-comparison-table__choose" aria-describedby="comparison-table-pro-price" data-ap-plan="Pro">Choose Pro</button></td>
          <td><button type="button" class="ap-btn ap-comparison-table__choose" aria-describedby="comparison-table-team-price" data-ap-plan="Team">Choose Team</button></td>
        </tr>
      </tfoot>
    </table>
  </div>

  <p class="ap-comparison-table__chosen" role="status" data-ap-chosen></p>
</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

    Plans are th scope=col, features th scope=row and each group label th scope=rowgroup across all four columns, so every cell has a plan and a feature to go with it.

  • 1.4.1 Use of Color Level A

    Included and not included are a tick and a cross, two different shapes, each with a word; Recommended is a word, not just a tinted column.

  • 1.4.10 Reflow Level AA

    At narrow widths the table scrolls inside its own region, which reflow allows for data tables, while the heading, switch and buttons around it reflow.

  • 2.1.1 Keyboard Level A

    The scroll area takes focus, so the arrow keys scroll the table without a mouse or trackpad.

  • 2.4.6 Headings and Labels Level AA

    Each button names its plan, Choose Pro rather than a third Choose, and carries the price as its description.

  • 4.1.2 Name, Role, Value Level A

    The filter is a checkbox with role=switch, so it is announced as a switch that is on or off.

  • 4.1.3 Status Messages Level AA

    After filtering, the number of features left is a status message, announced without moving focus.

Usage

When to use it

Use it

  • Helping people choose one of a few plans or products by the same features, side by side.
  • Specification sheets where each column is an option and each row a property.

Use something else

  • More than four or five options: let people pick the ones to compare first.
  • One product's details: a description list reads better than a one-column table.
  • Pricing with a single plan worth explaining: cards with plain lists are easier to read.

Common failures

How it usually goes wrong

  • Ticks with nothing to say

    An icon-only tick is read as nothing, or as "image". Each tick and cross here carries a hidden Included or Not included.

  • Plan names in a row of divs

    Without th scope=col the cells have no header, so a screen reader says "Included" with no plan attached. Real headers name the plan for every cell.

  • Group labels as ordinary cells

    A section name in a td is read as data in the feature column. th scope=rowgroup across the row makes it the header of the rows below it.

  • Three buttons called Choose plan

    Pulled into a list of buttons, three identical names cannot be told apart. Choose Starter, Choose Pro and Choose Team can.

  • Recommended shown only by a tint

    A shaded column says nothing to people who cannot see the shade, or to a screen reader. The word Recommended sits in the plan's header.

  • Filtering in silence

    Rows that vanish with no message leave screen reader users unsure anything happened. The count of features left is announced.

Notes

Building it

  • Hide filtered rows with the hidden attribute, so the row count a screen reader reports is the true one. A row made invisible with opacity is still read.
  • The table scrolls inside a fixed-height region so its plan names can stick. If your table never scrolls sideways, drop the region and let the header stick to the page instead, below your site header.
  • Sticky cells need an opaque background and border-collapse: separate; with collapsed borders, some browsers let the borders scroll away from sticky cells.
  • Keep limits as visible text (Up to 5, 100 GB): a tick would hide the part people are comparing.
  • aria-describedby on each button adds the price after its name, so it is heard without being repeated in the button's text.

Sources: W3C WAI Tables Tutorial: Tables with irregular headers · HTML: the th element and its scope attribute

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