Press Tab through the row. Each ring takes the color and size of its element. A mouse click on a button shows no ring, and a click into the input does.
With a mouse the button changes on hover. On a touch screen, or with a stylus, the readout says the query does not match and a tap leaves nothing stuck.
Press and hold a button. It shrinks the moment you press, label and all, and eases back when you let go. With Reduce Motion on it still shrinks, with no easing.
Why it works, the rules and the credits2 reasons · 3 rules · support · credits
Why it works
A native button responds the instant a finger lands. :active does the same on every kind of input. Feedback that waits for click arrives when the finger leaves, and that reads as lag.
scale() shrinks the label and icon along with the button, so the whole control reads as pressed.
Rules
Keep the scale between 0.95 and 0.98.
The reset removes the browser's tap highlight. Without this entry a tap gives no feedback at all.
When feedback needs a script, listen for pointerdown, not click.
Use it for cards, list rows and tiles where the whole area should navigate. It replaces an <a> wrapped around the card, which makes a screen reader read every word as the link text, and it replaces a click handler on a div.
Click anywhere on the card, the corners and the paragraph included: the link count goes up. Click Save: only the button count goes up. Tab to the link: the ring is drawn round the whole card.
CodeHTML · CSS · 2 blocks · 24 lines
1
2
3
4
5
<articleclass="card"><h3><aclass="card-link"href="/reports/q3">Quarterly report</a></h3><p>Revenue grew in every region.</p><buttontype="button">Save</button></article>
Why it works, the rules and the credits4 reasons · 4 rules · support · credits
Why it works
The pseudo-element belongs to the link and covers the card, so a click anywhere activates the link. It is a real link, so open in new tab and the status bar URL still work.
The link text stays short. A screen reader announces "Quarterly report" and not the whole card.
:has() moves the focus ring from the small link to the card it activates.
Other controls sit above the overlay and keep their own clicks.
Rules
No element between the card and the link may be positioned. The overlay would size itself to that element.
The link's own ring is made transparent, never removed, as One focus ring with :focus-visible requires. Write the full 2px solid transparent. Safari ignores outline-color on its default ring.
Text under the overlay cannot be selected. Accept it.
The article with an image spans two columns. Only the heading before a paragraph loses its margin. The form lights up when you tab into its field. The page stops scrolling while the dialog is open, and does not shift sideways.
Why it works, the rules and the credits2 reasons · 3 rules · support · credits
Why it works
:has() matches an element by its descendants or following siblings, and it accepts state pseudo-classes. Content and form state can style any ancestor.
On the root it works as a page-wide listener. The last rule locks page scroll while a modal dialog is open and lifts the lock when it closes, with nothing to clean up.
Rules
:modal matches showModal() only, so a non-modal dialog does not lock the page.
Keep scrollbar-gutter: stable with the scroll lock. Without it the page shifts sideways when the scrollbar disappears. The reset sets it.
Use it for any multi-line input, such as a chat composer or a comment box. field-sizing: content sizes the textarea to its text, so there is no auto-grow script and no hidden mirror element.
The height snaps to each new line. To animate it, wrap the textarea and let a ResizeObserver copy its height onto the wrapper. The wrapper transitions.