Accessibility pattern · Interaction and motion

Content on hover or focus

Anything that appears on hover or focus must pass three tests: Escape hides it without moving anything, the pointer can move onto it, and it stays until you leave or dismiss it. Try the three popups below and the checks tick as you go; then switch on the broken version and watch them fail.

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

Order summary

Handloom cotton saree, indigo

Sold by Meera Handlooms

Meera Handlooms

Weavers in Varanasi since 1998

Rated 4.8 out of 5 by 1,240 buyers

₹2,450

Delivery Free on orders over ₹999. Express delivery in Bengaluru, Mumbai and Delhi costs ₹99 and arrives the next day.
₹49
Payment
CODCash on delivery: pay in cash or by UPI when the parcel arrives.
Total
₹2,499

The three conditions

  • Dismissible Not tried yet

    Press Escape while a popup shows. It should hide, and focus and the pointer stay where they are.

  • Hoverable Not tried yet

    Move the pointer from a trigger onto its popup. It should stay open.

  • Persistent Not tried yet

    Rest on a trigger for three seconds. The popup should stay until you leave or press Escape.

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 to a trigger and its popup appears. From the seller's name, Tab moves into the card's links; leaving the card hides it.
EscapeHides the open popup without moving focus. From inside the card, focus goes back to the seller's name. It stays hidden until you leave and come back.
EnterFollows the focused link, or shows the delivery preview from the info button, as a tap does.

Screen readers

What it announces

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

WhenExpected announcement
Focus reaches CODCOD, link. Cash on delivery: pay in cash or by UPI when the parcel arrives.
Focus reaches the info buttonAbout delivery charges, button. Free on orders over ₹999. Express delivery in Bengaluru, Mumbai and Delhi costs ₹99 and arrives the next day.
Tab moves from the seller's name into the cardMeera Handlooms, group. Visit the shop, link
Escape hides a popupNothing; focus stays on the trigger
A check changesHoverable: pass

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-hover" data-ap-hover-content>
  <div class="ap-hover__layout">
    <section class="ap-hover__order" aria-labelledby="hover-order-name">
      <h3 class="ap-hover__name" id="hover-order-name">Order summary</h3>

      <div class="ap-hover__item">
        <svg class="ap-hover__swatch" viewBox="0 0 48 48" aria-hidden="true" focusable="false"><rect class="ap-hover__cloth" width="48" height="48" rx="8"/><path class="ap-hover__weave" d="M0 34h48M0 39h48"/><path class="ap-hover__motif" d="M12 12l4 6-4 6-4-6zM28 12l4 6-4 6-4-6z"/></svg>
        <div class="ap-hover__item-text">
          <p class="ap-hover__item-name">Handloom cotton saree, indigo</p>
          <p class="ap-hover__seller-line">Sold by <a class="ap-hover__trigger ap-hover__seller" id="hover-seller" href="#hover-shop" data-ap-trigger="hover-card">Meera Handlooms</a></p>
          <div class="ap-hover__pop ap-hover__card" id="hover-card" role="group" aria-labelledby="hover-card-name" popover="manual">
            <div class="ap-hover__card-top">
              <span class="ap-hover__avatar" aria-hidden="true" translate="no">MH</span>
              <div>
                <p class="ap-hover__card-name" id="hover-card-name">Meera Handlooms</p>
                <p class="ap-hover__card-meta">Weavers in Varanasi since 1998</p>
              </div>
            </div>
            <p class="ap-hover__card-stat">Rated 4.8 out of 5 by 1,240 buyers</p>
            <div class="ap-hover__card-links">
              <a href="#hover-shop">Visit the shop</a>
              <a href="#hover-message">Message the seller</a>
            </div>
          </div>
        </div>
        <p class="ap-hover__price">₹2,450</p>
      </div>

      <dl class="ap-hover__lines">
        <div class="ap-hover__line">
          <dt class="ap-hover__label">Delivery
            <button type="button" class="ap-hover__trigger ap-hover__info" id="hover-info" aria-label="About delivery charges" aria-describedby="hover-preview" data-ap-trigger="hover-preview">
              <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M12 11v5M12 8h.01"/></svg>
            </button>
            <span class="ap-hover__pop ap-hover__tip" id="hover-preview" role="tooltip" popover="manual">Free on orders over ₹999. Express delivery in Bengaluru, Mumbai and Delhi costs ₹99 and arrives the next day.</span>
          </dt>
          <dd>₹49</dd>
        </div>
        <div class="ap-hover__line">
          <dt class="ap-hover__label">Payment</dt>
          <dd><a class="ap-hover__trigger ap-hover__term" id="hover-cod" href="#hover-glossary-cod" aria-describedby="hover-def" data-ap-trigger="hover-def">COD</a><span class="ap-hover__pop ap-hover__tip" id="hover-def" role="tooltip" popover="manual">Cash on delivery: pay in cash or by UPI when the parcel arrives.</span></dd>
        </div>
        <div class="ap-hover__line ap-hover__line--total">
          <dt>Total</dt>
          <dd>₹2,499</dd>
        </div>
      </dl>
      <button type="button" class="ap-btn ap-btn--primary ap-hover__place" data-ap-place>Place order</button>
      <p class="ap-hover__note" role="status"></p>
    </section>

    <section class="ap-hover__checks" aria-labelledby="hover-checks-name">
      <h3 class="ap-hover__checks-name" id="hover-checks-name">The three conditions</h3>
      <ul class="ap-hover__conds">
        <li class="ap-hover__cond" data-ap-cond="dismissible" data-state="idle">
          <svg class="ap-hover__mark" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle class="ap-hover__mark-idle" cx="12" cy="12" r="8"/><path class="ap-hover__mark-pass" d="m5 12.5 4.5 4.5L19 7.5"/><path class="ap-hover__mark-fail" d="M7 7l10 10M17 7 7 17"/></svg>
          <span class="ap-hover__cond-name">Dismissible</span>
          <span class="ap-hover__verdict" data-ap-verdict>Not tried yet</span>
          <p class="ap-hover__how" data-ap-result>Press Escape while a popup shows. It should hide, and focus and the pointer stay where they are.</p>
        </li>
        <li class="ap-hover__cond" data-ap-cond="hoverable" data-state="idle">
          <svg class="ap-hover__mark" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle class="ap-hover__mark-idle" cx="12" cy="12" r="8"/><path class="ap-hover__mark-pass" d="m5 12.5 4.5 4.5L19 7.5"/><path class="ap-hover__mark-fail" d="M7 7l10 10M17 7 7 17"/></svg>
          <span class="ap-hover__cond-name">Hoverable</span>
          <span class="ap-hover__verdict" data-ap-verdict>Not tried yet</span>
          <p class="ap-hover__how" data-ap-result>Move the pointer from a trigger onto its popup. It should stay open.</p>
        </li>
        <li class="ap-hover__cond" data-ap-cond="persistent" data-state="idle">
          <svg class="ap-hover__mark" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle class="ap-hover__mark-idle" cx="12" cy="12" r="8"/><path class="ap-hover__mark-pass" d="m5 12.5 4.5 4.5L19 7.5"/><path class="ap-hover__mark-fail" d="M7 7l10 10M17 7 7 17"/></svg>
          <span class="ap-hover__cond-name">Persistent</span>
          <span class="ap-hover__verdict" data-ap-verdict>Not tried yet</span>
          <p class="ap-hover__how" data-ap-result>Rest on a trigger for three seconds. The popup should stay until you leave or press Escape.</p>
        </li>
      </ul>
      <p class="ap-hover__said" role="status"></p>
    </section>
  </div>
</div>

WCAG 2.2

What it meets

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

  • 1.4.13 Content on Hover or Focus Level AA

    Each popup can be dismissed with Escape without moving the pointer or focus, can be hovered across the gap from its trigger, and stays until hover and focus leave or Escape is pressed. The checks test all three live.

  • 2.1.1 Keyboard Level A

    Every popup appears on keyboard focus as well as hover, and the info button also shows its preview on Enter or a tap.

  • 2.4.3 Focus Order Level A

    The profile card follows its link in the DOM, so Tab goes from the name straight into the card's links and on to the rest of the order.

  • 2.4.11 Focus Not Obscured (Minimum) Level AA

    Popups open beside their trigger, never over it, so the element with focus stays in view.

  • 4.1.2 Name, Role, Value Level A

    The definition and the preview are role=tooltip and describe their triggers; the card, which holds links, is a named group.

Usage

When to use it

Use it

  • Short extra information people may want on the way past: a definition, a preview of where a link goes, a profile card.
  • Content that also exists elsewhere, so nobody who cannot hover misses anything they need.

Use something else

  • Information needed to finish the task, like a delivery fee that is only in a tooltip: put it on the page.
  • Long or complex content, or forms: open a popover or a dialog on click instead.
  • Touch-first screens with no hover: use a toggletip that opens on tap.

Common failures

How it usually goes wrong

  • It vanishes when you reach for it

    A gap between trigger and popup with no grace period closes it as the pointer crosses. People who zoom in need to move onto it to read it. A short delay and an invisible bridge keep it open.

  • No way to make it go away

    A popup that covers the next line and ignores Escape forces people to move the pointer, which may hide what they were reading. Escape must hide it without moving anything.

  • It closes on a timer

    A popup that disappears after two seconds is gone before slow readers finish. It stays until the pointer or focus leaves, or Escape.

  • Hover only

    Keyboard users never see a popup that only opens on mouseover, and their focus can never reach the links inside it. Show it on focus too, and put it next to its trigger in the DOM.

  • Links in a tooltip

    role=tooltip cannot hold interactive content. The profile card holds links, so it is a named group that focus can enter, not a tooltip.

Notes

Building it

  • popover=manual puts each popup in the top layer, so overflow on a parent cannot clip it, while it stays next to its trigger in the DOM and in the Tab order.
  • The bridge is an ::after on the popup that covers the gap toward the trigger, plus a 300 ms grace delay before hiding: both are needed, because pointers do not move in straight lines.
  • After Escape, a popup stays dismissed until the pointer and focus have both left its trigger and come back, so it does not reappear under a resting pointer.
  • The checks watch real events: an Escape while a popup is open, the pointer reaching a popup, and a popup that stays open for three seconds or closes on a timer.
  • The demo's links point to #… because their pages do not exist here; in a real site they are ordinary links.

Sources: Understanding WCAG 2.2: Content on Hover or Focus · WAI-ARIA Authoring Practices: Tooltip · HTML: the popover 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