Accessibility pattern · Disclosure and content

FAQ

Each question is a heading with a button in it, so the FAQ shows up in the headings list and every answer opens from the keyboard. Search filters as you type, highlights what matched, and a status message says how many questions match without moving focus.

WCAG criteria
6
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.

9 questions

Orders and delivery

Returns and refunds

Payments and account

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 from the search field to Expand all, Collapse all and the popular links, then question by question.
EscapeIn the search field, clears the search and shows every question again.
Enter or SpaceOn a question, opens or closes its answer; focus stays on the question.
EnterOn a popular link, opens that question and moves focus to it.

Screen readers

What it announces

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

WhenExpected announcement
Typing return in the search field, then pausing3 questions match
Focus reaches a questionWhen will I get my refund?, button, collapsed, heading level 4
Enter opens itExpanded
A popular link is followedHow do I return an item?, button, expanded, heading level 4
The search matches nothing0 questions match

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-faq" data-ap-faq>
  <div class="ap-faq__tools">
    <label class="ap-faq__label" for="faq-search">Search the questions</label>
    <div class="ap-faq__field">
      <svg class="ap-faq__glass" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="6.5"/><path d="m20 20-4.2-4.2"/></svg>
      <input class="ap-faq__input" id="faq-search" type="search" autocomplete="off" spellcheck="false" />
    </div>
    <div class="ap-faq__bar">
      <p class="ap-faq__count" role="status"><span class="ap-faq__num" translate="no">9</span> <span data-faq-words>questions</span></p>
      <div class="ap-faq__all">
        <button type="button" class="ap-faq__btn" data-faq-expand>Expand all</button>
        <button type="button" class="ap-faq__btn" data-faq-collapse>Collapse all</button>
      </div>
    </div>
    <div class="ap-faq__popular">
      <p class="ap-faq__pop-k" id="faq-pop">Popular</p>
      <ul class="ap-faq__pop" aria-labelledby="faq-pop">
        <li><a href="#faq-return">Return an item</a></li>
        <li><a href="#faq-cod">Cash on delivery</a></li>
        <li><a href="#faq-password">Reset your password</a></li>
      </ul>
    </div>
  </div>

  <div class="ap-faq__group" data-faq-group>
    <h3 class="ap-faq__topic">Orders and delivery</h3>
    <div class="ap-faq__list">
      <div class="ap-faq__item" data-faq-item>
        <h4 class="ap-faq__q" id="faq-arrive">
          <button type="button" class="ap-faq__trigger" aria-expanded="false" aria-controls="faq-a-arrive">
            <span class="ap-faq__text">When will my order arrive?</span>
            <svg class="ap-faq__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path class="ap-faq__icon-v" d="M12 6v12"/><path d="M6 12h12"/></svg>
          </button>
        </h4>
        <div class="ap-faq__a" id="faq-a-arrive" hidden="until-found">
          <p>Orders inside India arrive in three to five working days, and remote pin codes can take up to eight. You get a tracking link by SMS and email when the parcel leaves.</p>
        </div>
      </div>
      <div class="ap-faq__item" data-faq-item>
        <h4 class="ap-faq__q" id="faq-address">
          <button type="button" class="ap-faq__trigger" aria-expanded="false" aria-controls="faq-a-address">
            <span class="ap-faq__text">Can I change my delivery address?</span>
            <svg class="ap-faq__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path class="ap-faq__icon-v" d="M12 6v12"/><path d="M6 12h12"/></svg>
          </button>
        </h4>
        <div class="ap-faq__a" id="faq-a-address" hidden="until-found">
          <p>Yes, until the order is packed. Open the order in your account and choose Change address. Once it has shipped, write to us and we will ask the courier.</p>
        </div>
      </div>
      <div class="ap-faq__item" data-faq-item>
        <h4 class="ap-faq__q" id="faq-cod">
          <button type="button" class="ap-faq__trigger" aria-expanded="false" aria-controls="faq-a-cod">
            <span class="ap-faq__text">Do you offer cash on delivery?</span>
            <svg class="ap-faq__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path class="ap-faq__icon-v" d="M12 6v12"/><path d="M6 12h12"/></svg>
          </button>
        </h4>
        <div class="ap-faq__a" id="faq-a-cod" hidden="until-found">
          <p>Yes, on orders up to ₹10,000 to most pin codes. A ₹40 handling fee applies, and you can pay the courier in cash or by UPI.</p>
        </div>
      </div>
    </div>
  </div>

  <div class="ap-faq__group" data-faq-group>
    <h3 class="ap-faq__topic">Returns and refunds</h3>
    <div class="ap-faq__list">
      <div class="ap-faq__item" data-faq-item>
        <h4 class="ap-faq__q" id="faq-return">
          <button type="button" class="ap-faq__trigger" aria-expanded="false" aria-controls="faq-a-return">
            <span class="ap-faq__text">How do I return an item?</span>
            <svg class="ap-faq__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path class="ap-faq__icon-v" d="M12 6v12"/><path d="M6 12h12"/></svg>
          </button>
        </h4>
        <div class="ap-faq__a" id="faq-a-return" hidden="until-found">
          <p>Return anything unused within 30 days. Choose Return item on the order page, pick a pickup slot, and keep the item in its original packaging.</p>
        </div>
      </div>
      <div class="ap-faq__item" data-faq-item>
        <h4 class="ap-faq__q" id="faq-refund">
          <button type="button" class="ap-faq__trigger" aria-expanded="false" aria-controls="faq-a-refund">
            <span class="ap-faq__text">When will I get my refund?</span>
            <svg class="ap-faq__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path class="ap-faq__icon-v" d="M12 6v12"/><path d="M6 12h12"/></svg>
          </button>
        </h4>
        <div class="ap-faq__a" id="faq-a-refund" hidden="until-found">
          <p>Refunds start the day the return reaches our warehouse. UPI and card refunds take three to seven working days to show; cash on delivery orders are refunded to your bank account.</p>
        </div>
      </div>
      <div class="ap-faq__item" data-faq-item>
        <h4 class="ap-faq__q" id="faq-exchange">
          <button type="button" class="ap-faq__trigger" aria-expanded="false" aria-controls="faq-a-exchange">
            <span class="ap-faq__text">Can I exchange an item for another size?</span>
            <svg class="ap-faq__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path class="ap-faq__icon-v" d="M12 6v12"/><path d="M6 12h12"/></svg>
          </button>
        </h4>
        <div class="ap-faq__a" id="faq-a-exchange" hidden="until-found">
          <p>Yes. Choose Exchange instead of Return and pick the new size. We send it as soon as the pickup is done, at no extra cost.</p>
        </div>
      </div>
    </div>
  </div>

  <div class="ap-faq__group" data-faq-group>
    <h3 class="ap-faq__topic">Payments and account</h3>
    <div class="ap-faq__list">
      <div class="ap-faq__item" data-faq-item>
        <h4 class="ap-faq__q" id="faq-pay">
          <button type="button" class="ap-faq__trigger" aria-expanded="false" aria-controls="faq-a-pay">
            <span class="ap-faq__text">Which payment methods do you accept?</span>
            <svg class="ap-faq__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path class="ap-faq__icon-v" d="M12 6v12"/><path d="M6 12h12"/></svg>
          </button>
        </h4>
        <div class="ap-faq__a" id="faq-a-pay" hidden="until-found">
          <p>UPI, debit and credit cards, net banking and cash on delivery. Card EMI is available on orders above ₹3,000.</p>
        </div>
      </div>
      <div class="ap-faq__item" data-faq-item>
        <h4 class="ap-faq__q" id="faq-card">
          <button type="button" class="ap-faq__trigger" aria-expanded="false" aria-controls="faq-a-card">
            <span class="ap-faq__text">Is it safe to save my card?</span>
            <svg class="ap-faq__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path class="ap-faq__icon-v" d="M12 6v12"/><path d="M6 12h12"/></svg>
          </button>
        </h4>
        <div class="ap-faq__a" id="faq-a-card" hidden="until-found">
          <p>Your card is stored by our payment partner as a token that only works on this shop, never as the card number. You can delete it at any time under Account, Payments.</p>
        </div>
      </div>
      <div class="ap-faq__item" data-faq-item>
        <h4 class="ap-faq__q" id="faq-password">
          <button type="button" class="ap-faq__trigger" aria-expanded="false" aria-controls="faq-a-password">
            <span class="ap-faq__text">How do I reset my password?</span>
            <svg class="ap-faq__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path class="ap-faq__icon-v" d="M12 6v12"/><path d="M6 12h12"/></svg>
          </button>
        </h4>
        <div class="ap-faq__a" id="faq-a-password" hidden="until-found">
          <p>Choose Forgot password on the sign-in page and enter your email address. The reset link works for 30 minutes.</p>
        </div>
      </div>
    </div>
  </div>

  <div class="ap-faq__empty" data-faq-empty hidden>
    <p>No questions match those words. Try fewer or shorter words.</p>
    <button type="button" class="ap-faq__btn" data-faq-clear>Clear the search</button>
  </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

    Topics are h3 and questions h4, so the FAQ's outline is in the headings list; the search field has a visible label.

  • 2.1.1 Keyboard Level A

    Searching, opening answers, Expand all and the popular links all work from the keyboard.

  • 2.4.3 Focus Order Level A

    Following a popular link moves focus to the question it names, so the next Tab continues from there.

  • 3.2.2 On Input Level A

    Typing filters the list but never moves focus or opens anything; nothing changes context as you type.

  • 4.1.2 Name, Role, Value Level A

    Each question's button carries aria-expanded and aria-controls, so its open state is exposed and announced.

  • 4.1.3 Status Messages Level AA

    The match count is a status message, announced politely once typing pauses, without taking focus.

Usage

When to use it

Use it

  • A help page of short answers that people search or scan for one question.
  • Answers grouped by topic, where a link to one question is worth sharing.

Use something else

  • Information everybody needs: put it in the page or the flow, not behind a question.
  • Long answers with steps and screenshots: give each one its own help page.
  • A handful of questions: a plain list of headings and paragraphs needs no search.

Common failures

How it usually goes wrong

  • Questions that are not headings

    Bold paragraphs look like headings but cannot be jumped to. Real headings let people skim the FAQ by heading, as many screen reader users do.

  • A count nobody hears

    A number that changes on screen says nothing to a screen reader user. The count here is a status message, so the result is announced.

  • Announcing every keystroke

    A live region updated on each key interrupts itself. The count here waits for a short pause in typing before it changes.

  • Deep links that land on a closed answer

    A link to a question that only scrolls there shows the question, not the answer. The script opens it and moves focus to it.

  • Filtered questions still in the page

    Questions faded out with opacity are still read out and still reached by Tab. Filtered questions here use the hidden attribute.

  • Matches you cannot see

    A search that matches inside a closed answer, then shows the question with nothing marked, looks broken. Each question here says when the match is in its answer.

Notes

Building it

  • The search matches every word you type, in the question or the answer, ignoring case, and wraps each match in mark.
  • Collapsed answers use hidden="until-found", so find-in-page still works; filtered-out questions use plain hidden, because the reader asked for them to go.
  • Expand all opens only the questions the current search shows.
  • Deep links work on load and on hashchange. Keep the ids stable: people bookmark them and paste them into chats.
  • Skip role=region on the answers: with nine of them the page fills with landmarks nobody needs.

Sources: WAI-ARIA Authoring Practices: Disclosure (FAQ example) · WCAG 2.2 Understanding 4.1.3: Status Messages

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