Checkout redesign, round 2
Asha Rao
Product designer, Pune
Available · 4:30 pm her time
- Team
- Checkout and payments
- Reviews this month
- 12
Accessibility pattern · Overlays
The popover attribute puts a card in the top layer with light dismiss and Escape built in, and popovertarget wires a button to it with no script. CSS anchor positioning keeps it beside the button; the script keeps aria-expanded in step and moves focus in only when the card has something to use.
Live demo
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.
Asha Rao
Product designer, Pune
Available · 4:30 pm her time
The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.
Keyboard
Every action works without a pointer.
| Key | What it does |
|---|---|
| Enter or Space | On the name button, opens the card and moves focus to its first button; pressed again, closes it. |
| Tab or ShiftTab | Moves through the card's buttons, which come straight after the name button; tabbing on past the card closes it. |
| Escape | Closes the card and returns focus to the name button. |
| Enter or Space | On Follow, turns following on or off. |
Screen readers
Written from the roles, names and states in the markup.
| When | Expected announcement |
|---|---|
| Focus reaches the name button | Asha Rao, button, collapsed, has pop-up dialog |
| The card opens | Asha Rao, dialog. Message, button |
| Follow is pressed | Follow, toggle button, pressed |
| Escape closes the card | Asha Rao, button, collapsed |
| Message is pressed | Opened a message to Asha Rao |
These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.
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.
<div class="ap-popover" data-ap-popover>
<article class="ap-popover__doc">
<div class="ap-popover__doc-top">
<span class="ap-popover__doc-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" focusable="false"><path d="M6 3h8l4 4v14H6Z"/><path d="M14 3v4h4M9 12h6M9 16h4"/></svg>
</span>
<div>
<h3 class="ap-popover__doc-name">Checkout redesign, round 2</h3>
<p class="ap-popover__doc-meta">Design review · Due Friday</p>
</div>
</div>
<div class="ap-popover__by">
<span class="ap-popover__by-k">Reviewer</span>
<button type="button" class="ap-popover__trigger" popovertarget="popover-card" aria-haspopup="dialog" aria-expanded="false">
<span class="ap-popover__avatar" aria-hidden="true">AR</span>
<span>Asha Rao</span>
<svg class="ap-popover__chev" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6"/></svg>
</button>
<div class="ap-popover__card" id="popover-card" popover role="dialog" aria-labelledby="popover-name">
<div class="ap-popover__head">
<span class="ap-popover__avatar ap-popover__avatar--lg" aria-hidden="true">AR</span>
<div>
<p class="ap-popover__name" id="popover-name">Asha Rao</p>
<p class="ap-popover__role">Product designer, Pune</p>
</div>
</div>
<p class="ap-popover__presence">Available · 4:30 pm her time</p>
<dl class="ap-popover__facts">
<div><dt>Team</dt><dd>Checkout and payments</dd></div>
<div><dt>Reviews this month</dt><dd>12</dd></div>
</dl>
<div class="ap-popover__actions">
<button type="button" class="ap-btn ap-btn--primary" data-ap-message>
<svg class="ap-btn__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 5h16v11H9.5L4 20Z"/></svg>
Message
</button>
<button type="button" class="ap-btn ap-popover__follow" data-ap-follow aria-pressed="false">
<svg class="ap-btn__icon ap-popover__plus" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 5v14M5 12h14"/></svg>
<svg class="ap-btn__icon ap-popover__tick" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg>
Follow
</button>
</div>
</div>
</div>
</article>
<p class="ap-popover__status" role="status" data-ap-status></p>
</div>
/* Shared primitives: buttons and form fields used across the patterns.
Uses the --ap-* design tokens. */
.ap-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 40px;
padding: 0 16px;
border: 1px solid var(--ap-border-strong);
border-radius: var(--ap-radius-sm);
background: var(--ap-surface);
color: var(--ap-text);
font: inherit;
font-weight: 600;
line-height: 1.2;
text-decoration: none;
white-space: nowrap;
cursor: pointer;
transition: background-color var(--ap-duration) var(--ap-ease), border-color var(--ap-duration) var(--ap-ease), box-shadow var(--ap-duration) var(--ap-ease);
}
.ap-btn:hover {
background: var(--ap-surface-2);
}
.ap-btn:focus-visible {
outline: 2px solid var(--ap-focus);
outline-offset: 2px;
}
.ap-btn:disabled,
.ap-btn[aria-disabled="true"] {
opacity: .55;
cursor: not-allowed;
}
.ap-btn--primary {
border-color: var(--ap-accent);
background: var(--ap-accent);
color: var(--ap-on-accent);
box-shadow: var(--ap-shadow-sm);
}
.ap-btn--primary:hover {
border-color: var(--ap-accent-hover);
background: var(--ap-accent-hover);
}
.ap-btn--danger {
border-color: var(--ap-danger);
background: var(--ap-danger);
color: var(--ap-surface);
}
.ap-btn--danger:hover {
filter: brightness(1.08);
}
.ap-btn--ghost {
border-color: transparent;
background: transparent;
}
.ap-btn--ghost:hover {
background: var(--ap-surface-2);
}
.ap-btn__icon {
width: 18px;
height: 18px;
flex-shrink: 0;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.ap-field {
display: grid;
gap: 6px;
}
.ap-label {
color: var(--ap-text);
font-weight: 600;
font-size: .9375rem;
}
.ap-hint {
margin: 0;
color: var(--ap-text-3);
font-size: .875rem;
line-height: 1.45;
}
.ap-error {
display: flex;
align-items: flex-start;
gap: 6px;
margin: 0;
color: var(--ap-danger);
font-size: .875rem;
font-weight: 600;
line-height: 1.45;
}
.ap-input {
width: 100%;
min-height: 44px;
padding: 10px 12px;
border: 1px solid var(--ap-border-strong);
border-radius: var(--ap-radius-sm);
background: var(--ap-surface);
color: var(--ap-text);
font: inherit;
line-height: 1.4;
transition: border-color var(--ap-duration) var(--ap-ease), box-shadow var(--ap-duration) var(--ap-ease);
}
.ap-input::placeholder {
color: var(--ap-text-3);
}
.ap-input:hover {
border-color: var(--ap-text-2);
}
.ap-input:focus-visible {
outline: 2px solid var(--ap-focus);
outline-offset: 1px;
border-color: var(--ap-focus);
}
.ap-input[aria-invalid="true"] {
border-color: var(--ap-danger);
box-shadow: inset 4px 0 0 var(--ap-danger);
}
textarea.ap-input {
resize: vertical;
min-height: 88px;
}
@media (prefers-reduced-motion: reduce) {
.ap-btn,
.ap-input { transition: none; }
}
/* Popover. Uses the --ap-* design tokens and the shared primitives. */
.ap-popover {
display: grid;
justify-items: center;
gap: 14px;
}
/* The document card the trigger lives in */
.ap-popover__doc {
width: min(100%, 440px);
padding: 20px 22px;
border: 1px solid var(--ap-border);
border-radius: var(--ap-radius-lg);
background: var(--ap-surface);
box-shadow: var(--ap-shadow-md);
}
.ap-popover__doc-top {
display: flex;
align-items: flex-start;
gap: 14px;
}
.ap-popover__doc-icon {
display: grid;
place-items: center;
width: 40px;
height: 40px;
flex-shrink: 0;
border-radius: var(--ap-radius-sm);
background: var(--ap-accent-soft);
color: var(--ap-accent-soft-text);
}
.ap-popover__doc-icon svg {
width: 20px;
height: 20px;
fill: none;
stroke: currentColor;
stroke-width: 1.8;
stroke-linecap: round;
stroke-linejoin: round;
}
.ap-popover__doc-name {
margin: 0;
font-size: 1.0625rem;
font-weight: 650;
line-height: 1.35;
}
.ap-popover__doc-meta {
margin: 2px 0 0;
color: var(--ap-text-3);
font-size: .875rem;
}
.ap-popover__by {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 12px;
margin-top: 16px;
padding-top: 14px;
border-top: 1px solid var(--ap-border);
}
.ap-popover__by-k {
color: var(--ap-text-3);
font-size: .875rem;
}
/* The trigger: a person chip */
.ap-popover__trigger {
display: inline-flex;
align-items: center;
gap: 8px;
min-height: 40px;
padding: 0 10px 0 4px;
border: 1px solid var(--ap-border-strong);
border-radius: var(--ap-radius-full);
background: var(--ap-surface);
color: var(--ap-text);
font: inherit;
font-weight: 600;
cursor: pointer;
transition: background-color var(--ap-duration) var(--ap-ease);
}
.ap-popover__trigger:hover,
.ap-popover__trigger[aria-expanded="true"] {
background: var(--ap-surface-2);
}
.ap-popover__trigger:focus-visible {
outline: 2px solid var(--ap-focus);
outline-offset: 2px;
}
.ap-popover__chev {
width: 16px;
height: 16px;
fill: none;
stroke: var(--ap-text-3);
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
transition: transform var(--ap-duration) var(--ap-ease);
}
.ap-popover__trigger[aria-expanded="true"] .ap-popover__chev {
transform: rotate(180deg);
}
.ap-popover__avatar {
display: grid;
place-items: center;
width: 30px;
height: 30px;
flex-shrink: 0;
border-radius: var(--ap-radius-full);
background: var(--ap-accent);
color: var(--ap-on-accent);
font-size: .75rem;
font-weight: 700;
letter-spacing: .02em;
}
.ap-popover__avatar--lg {
width: 48px;
height: 48px;
font-size: 1rem;
}
/* The popover card. Positioned by CSS anchor positioning where supported;
elsewhere the script sets top and left. */
.ap-popover__card {
position: fixed;
inset: auto;
width: min(320px, 100vw - 32px);
margin: 0;
padding: 18px;
border: 1px solid var(--ap-border);
border-radius: var(--ap-radius-lg);
background: var(--ap-surface);
color: var(--ap-text);
box-shadow: var(--ap-shadow-lg);
opacity: 1;
transform: none;
transition: opacity 150ms var(--ap-ease), transform 150ms var(--ap-ease), overlay 150ms allow-discrete, display 150ms allow-discrete;
}
.ap-popover__card:not(:popover-open) {
opacity: 0;
transform: translateY(-4px);
}
@starting-style {
.ap-popover__card:popover-open {
opacity: 0;
transform: translateY(-4px);
}
}
@supports (anchor-name: --a) {
.ap-popover__trigger {
anchor-name: --ap-popover-anchor;
}
.ap-popover__card {
position-anchor: --ap-popover-anchor;
position-area: block-end span-inline-end;
margin-block-start: 8px;
/* Flip when it would leave the screen; on a narrow screen, centre it. */
position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline, block-end, block-start;
}
}
.ap-popover__head {
display: flex;
align-items: center;
gap: 12px;
}
.ap-popover__card p {
margin: 0;
}
.ap-popover__name {
font-size: 1.0625rem;
font-weight: 650;
line-height: 1.3;
}
.ap-popover__role {
color: var(--ap-text-3);
font-size: .875rem;
}
.ap-popover__card .ap-popover__presence {
display: flex;
align-items: center;
gap: 8px;
margin-top: 14px;
color: var(--ap-text-2);
font-size: .875rem;
}
.ap-popover__presence::before {
content: "";
width: 8px;
height: 8px;
flex-shrink: 0;
border-radius: var(--ap-radius-full);
background: var(--ap-success);
}
.ap-popover__facts {
display: grid;
gap: 8px;
margin: 14px 0 0;
padding: 12px 0 0;
border-top: 1px solid var(--ap-border);
font-size: .875rem;
}
.ap-popover__facts div {
display: flex;
justify-content: space-between;
gap: 12px;
}
.ap-popover__facts dt {
color: var(--ap-text-3);
}
.ap-popover__facts dd {
margin: 0;
font-weight: 600;
text-align: end;
}
.ap-popover__actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
margin-top: 16px;
}
.ap-popover__follow .ap-popover__tick,
.ap-popover__follow[aria-pressed="true"] .ap-popover__plus {
display: none;
}
.ap-popover__follow[aria-pressed="true"] .ap-popover__tick {
display: block;
}
.ap-popover__follow[aria-pressed="true"] {
border-color: var(--ap-accent);
background: var(--ap-accent-soft);
color: var(--ap-accent-soft-text);
}
.ap-popover__status {
min-height: 1.5em;
margin: 0;
color: var(--ap-success);
font-weight: 600;
}
@media (prefers-reduced-motion: reduce) {
.ap-popover__card,
.ap-popover__trigger,
.ap-popover__chev { transition: none; }
}
/**
* Popover: a card anchored to its button, on the native popover attribute.
*
* Markup: [data-ap-popover] holding a button[popovertarget][aria-expanded]
* followed directly by its div[popover][role=dialog][aria-labelledby], and a
* role=status paragraph[data-ap-status]. The browser opens and closes the
* card (click, Escape, a click outside); this script keeps aria-expanded in
* step, moves focus in when the card has controls and back out when it
* closes, and places the card where CSS anchor positioning is missing.
*/
const FOCUSABLE = "a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex='-1'])";
export function init(root) {
const trigger = root.querySelector("[popovertarget]");
const card = root.querySelector("[popover]");
const status = root.querySelector("[data-ap-status]");
const anchored = CSS.supports("anchor-name: --a");
let tracking = null;
// Fallback placement: below the button, or above it when there is no room,
// kept inside the viewport.
function place() {
const r = trigger.getBoundingClientRect();
const gap = 8;
const edge = 8;
const below = r.bottom + gap + card.offsetHeight <= innerHeight - edge || r.top - gap - card.offsetHeight < edge;
card.style.top = `${below ? r.bottom + gap : r.top - gap - card.offsetHeight}px`;
card.style.left = `${Math.max(edge, Math.min(r.left, innerWidth - card.offsetWidth - edge))}px`;
}
function onToggle(event) {
const open = event.newState === "open";
trigger.setAttribute("aria-expanded", String(open));
if (open) {
if (!anchored) {
place();
tracking = new AbortController();
addEventListener("scroll", place, { capture: true, passive: true, signal: tracking.signal });
addEventListener("resize", place, { signal: tracking.signal });
}
// Move focus in only when there is something to use.
card.querySelector(FOCUSABLE)?.focus();
} else {
tracking?.abort();
tracking = null;
// Focus was inside the card and has nowhere to go: give it back.
const active = document.activeElement;
if (!active || active === document.body || card.contains(active)) trigger.focus({ preventScroll: true });
}
}
// Tabbing on past the card closes it, so it never sits over what has focus.
function onFocusOut(event) {
const to = event.relatedTarget;
if (to && !card.contains(to) && to !== trigger && card.matches(":popover-open")) card.hidePopover();
}
function onClick(event) {
const toggle = event.target.closest("[data-ap-follow]");
if (toggle) {
toggle.setAttribute("aria-pressed", String(toggle.getAttribute("aria-pressed") !== "true"));
} else if (event.target.closest("[data-ap-message]")) {
card.hidePopover();
trigger.focus();
status.textContent = "Opened a message to Asha Rao";
}
}
function onTriggerClick() {
status.textContent = "";
}
card.addEventListener("toggle", onToggle);
card.addEventListener("focusout", onFocusOut);
card.addEventListener("click", onClick);
trigger.addEventListener("click", onTriggerClick);
return () => {
card.removeEventListener("toggle", onToggle);
if (card.matches(":popover-open")) card.hidePopover();
tracking?.abort();
card.removeEventListener("focusout", onFocusOut);
card.removeEventListener("click", onClick);
trigger.removeEventListener("click", onTriggerClick);
};
}
for (const root of document.querySelectorAll("[data-ap-popover]")) init(root);
/* Design tokens for the pattern components. Light by default, dark when the
system asks for it; set data-theme="dark" on :root to force dark. */
:root {
--ap-radius-sm: 8px;
--ap-radius: 12px;
--ap-radius-lg: 16px;
--ap-radius-full: 999px;
--ap-font: "Instrument Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--ap-mono: "JetBrains Mono", ui-monospace, "Cascadia Mono", "SF Mono", Consolas, monospace;
--ap-ease: cubic-bezier(.2, .8, .2, 1);
--ap-duration: 180ms;
--ap-target: 44px;
--ap-bg: #F4F4F5;
--ap-surface: #FFFFFF;
--ap-surface-2: #F4F4F5;
--ap-surface-3: #E4E4E7;
--ap-border: #E4E4E7;
--ap-border-strong: #76767F;
--ap-text: #18181B;
--ap-text-2: #3F3F46;
--ap-text-3: #5E5E66;
--ap-accent: #4F46E5;
--ap-accent-hover: #4338CA;
--ap-on-accent: #FFFFFF;
--ap-accent-text: #4338CA;
--ap-accent-soft: #EEF2FF;
--ap-accent-soft-text: #3730A3;
--ap-focus: #4F46E5;
--ap-danger: #B91C1C;
--ap-danger-soft: #FEF2F2;
--ap-success: #15803D;
--ap-success-soft: #F0FDF4;
--ap-warning: #A15C07;
--ap-warning-soft: #FEFCE8;
--ap-info: #1D4ED8;
--ap-info-soft: #EFF6FF;
--ap-scrim: rgb(9 9 11 / .48);
--ap-shadow-sm: 0 1px 2px rgb(9 9 11 / .06);
--ap-shadow-md: 0 1px 2px rgb(9 9 11 / .05), 0 6px 16px -4px rgb(9 9 11 / .1);
--ap-shadow-lg: 0 2px 6px rgb(9 9 11 / .06), 0 20px 40px -12px rgb(9 9 11 / .22);
--ap-light-bg: #F4F4F5;
--ap-light-surface: #FFFFFF;
--ap-light-surface-2: #F4F4F5;
--ap-light-surface-3: #E4E4E7;
--ap-light-border: #E4E4E7;
--ap-light-border-strong: #76767F;
--ap-light-text: #18181B;
--ap-light-text-2: #3F3F46;
--ap-light-text-3: #5E5E66;
--ap-light-accent: #4F46E5;
--ap-light-accent-hover: #4338CA;
--ap-light-on-accent: #FFFFFF;
--ap-light-accent-text: #4338CA;
--ap-light-accent-soft: #EEF2FF;
--ap-light-accent-soft-text: #3730A3;
--ap-light-focus: #4F46E5;
--ap-light-danger: #B91C1C;
--ap-light-danger-soft: #FEF2F2;
--ap-light-success: #15803D;
--ap-light-success-soft: #F0FDF4;
--ap-light-warning: #A15C07;
--ap-light-warning-soft: #FEFCE8;
--ap-light-info: #1D4ED8;
--ap-light-info-soft: #EFF6FF;
--ap-light-scrim: rgb(9 9 11 / .48);
--ap-light-shadow-sm: 0 1px 2px rgb(9 9 11 / .06);
--ap-light-shadow-md: 0 1px 2px rgb(9 9 11 / .05), 0 6px 16px -4px rgb(9 9 11 / .1);
--ap-light-shadow-lg: 0 2px 6px rgb(9 9 11 / .06), 0 20px 40px -12px rgb(9 9 11 / .22);
--ap-dark-bg: #09090B;
--ap-dark-surface: #18181B;
--ap-dark-surface-2: #27272A;
--ap-dark-surface-3: #3F3F46;
--ap-dark-border: #2E2E33;
--ap-dark-border-strong: #8E8E97;
--ap-dark-text: #FAFAFA;
--ap-dark-text-2: #D4D4D8;
--ap-dark-text-3: #A1A1AA;
--ap-dark-accent: #818CF8;
--ap-dark-accent-hover: #A5B4FC;
--ap-dark-on-accent: #0C0A1F;
--ap-dark-accent-text: #A5B4FC;
--ap-dark-accent-soft: #1E1B4B;
--ap-dark-accent-soft-text: #C7D2FE;
--ap-dark-focus: #A5B4FC;
--ap-dark-danger: #F87171;
--ap-dark-danger-soft: #2A1215;
--ap-dark-success: #4ADE80;
--ap-dark-success-soft: #0F2A1A;
--ap-dark-warning: #FACC15;
--ap-dark-warning-soft: #2A2410;
--ap-dark-info: #60A5FA;
--ap-dark-info-soft: #0F1D33;
--ap-dark-scrim: rgb(0 0 0 / .62);
--ap-dark-shadow-sm: 0 1px 2px rgb(0 0 0 / .4);
--ap-dark-shadow-md: 0 1px 2px rgb(0 0 0 / .4), 0 8px 20px -6px rgb(0 0 0 / .5);
--ap-dark-shadow-lg: 0 2px 8px rgb(0 0 0 / .45), 0 24px 48px -12px rgb(0 0 0 / .7);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--ap-bg: #09090B;
--ap-surface: #18181B;
--ap-surface-2: #27272A;
--ap-surface-3: #3F3F46;
--ap-border: #2E2E33;
--ap-border-strong: #8E8E97;
--ap-text: #FAFAFA;
--ap-text-2: #D4D4D8;
--ap-text-3: #A1A1AA;
--ap-accent: #818CF8;
--ap-accent-hover: #A5B4FC;
--ap-on-accent: #0C0A1F;
--ap-accent-text: #A5B4FC;
--ap-accent-soft: #1E1B4B;
--ap-accent-soft-text: #C7D2FE;
--ap-focus: #A5B4FC;
--ap-danger: #F87171;
--ap-danger-soft: #2A1215;
--ap-success: #4ADE80;
--ap-success-soft: #0F2A1A;
--ap-warning: #FACC15;
--ap-warning-soft: #2A2410;
--ap-info: #60A5FA;
--ap-info-soft: #0F1D33;
--ap-scrim: rgb(0 0 0 / .62);
--ap-shadow-sm: 0 1px 2px rgb(0 0 0 / .4);
--ap-shadow-md: 0 1px 2px rgb(0 0 0 / .4), 0 8px 20px -6px rgb(0 0 0 / .5);
--ap-shadow-lg: 0 2px 8px rgb(0 0 0 / .45), 0 24px 48px -12px rgb(0 0 0 / .7);
}
}
:root[data-theme="dark"] {
--ap-bg: #09090B;
--ap-surface: #18181B;
--ap-surface-2: #27272A;
--ap-surface-3: #3F3F46;
--ap-border: #2E2E33;
--ap-border-strong: #8E8E97;
--ap-text: #FAFAFA;
--ap-text-2: #D4D4D8;
--ap-text-3: #A1A1AA;
--ap-accent: #818CF8;
--ap-accent-hover: #A5B4FC;
--ap-on-accent: #0C0A1F;
--ap-accent-text: #A5B4FC;
--ap-accent-soft: #1E1B4B;
--ap-accent-soft-text: #C7D2FE;
--ap-focus: #A5B4FC;
--ap-danger: #F87171;
--ap-danger-soft: #2A1215;
--ap-success: #4ADE80;
--ap-success-soft: #0F2A1A;
--ap-warning: #FACC15;
--ap-warning-soft: #2A2410;
--ap-info: #60A5FA;
--ap-info-soft: #0F1D33;
--ap-scrim: rgb(0 0 0 / .62);
--ap-shadow-sm: 0 1px 2px rgb(0 0 0 / .4);
--ap-shadow-md: 0 1px 2px rgb(0 0 0 / .4), 0 8px 20px -6px rgb(0 0 0 / .5);
--ap-shadow-lg: 0 2px 8px rgb(0 0 0 / .45), 0 24px 48px -12px rgb(0 0 0 / .7);
}
WCAG 2.2
The success criteria this pattern takes care of, and how.
The card sits straight after its button in the source, so reading order and Tab order match what is on screen.
The card is never wider than the screen, and anchor positioning flips it above or to the other side when there is no room.
Opening, closing and every button in the card work from the keyboard; Escape comes with the popover attribute.
Focus moves to the card's first button on open and back to the name button when the card closes with focus inside.
The button carries aria-expanded, the card is a dialog named after the person, and Follow reports aria-pressed.
Starting a message is confirmed in a status line that is announced without moving focus.
Usage
Common failures
Touch screens, keyboards and screen readers cannot hover. This card opens on click, Enter or Space.
A popup appended to the end of the body is read last and reached last with Tab. Keep it right after its button; the top layer handles stacking.
When a card closes with focus inside it, focus falls back to the start of the page. Here it goes back to the name button.
Pulling focus into a card that only holds text takes people away from where they were. Focus moves in only when there is a control to use.
Without aria-expanded, a screen reader user cannot tell that the button opened anything or that it is still open.
A card positioned inside a box with overflow: hidden gets cut off. The top layer lifts it out, and the fallbacks keep it on screen.
Notes
Sources: HTML: the popover attribute · CSS Anchor Positioning
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