# Card actions that only appear on hover

Source: https://easeweb.dev/learn/hover-actions
Topics: Drag and drop > Actions that appear on hover can be reached by keyboard and touch (WCAG 2.1.1, 1.4.13)
The fix: Either keep the reveal and add every other way in: show the actions on :focus-within as well as :hover, show them all the time where there is no hover, and give them space of their own so they never cover the card. Or stop hiding them: draw the actions small and quiet, and show them on every card all the time.
Test with: keyboard, mouse, touch, zoom, screen reader
References: https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html https://www.w3.org/WAI/WCAG22/Understanding/content-on-hover-or-focus.html https://www.w3.org/WAI/WCAG22/Techniques/failures/F54 https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-within https://developer.mozilla.org/en-US/docs/Web/CSS/@media/hover

This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.

## What happens

A shop's product grid is drawn clean: a photo, a name and a price on each card, and nothing else. Rest the pointer on a card and a row of actions fades in under the price: Save, and Add to bag. Move away and it is gone. The design keeps the grid quiet, and the actions are there for anyone with a mouse.

Put the mouse aside and press Tab. Focus moves from one card's name to the next card's name. Save and Add to bag are never reached, because the row is hidden with `visibility: hidden` until the card is hovered, and a hidden button takes no focus. On a phone, there is no pointer to rest: a tap on the card's photo or name opens the product page, and the actions never show on the grid at all.

A second version of the same design hides the row with `opacity: 0` instead, and slides it in over the bottom of the card, on top of the price. Now Tab does reach Save, but the button is fully transparent while focus is on it, so a keyboard user sees nothing change and doesn't know what Enter would do. Under a magnifier, the row covers the price as soon as the pointer arrives, and Escape does nothing.

## Why it fails

The actions exist only for a resting pointer. WCAG 2.1.1 Keyboard asks for every function to be operable from the keyboard, and failure F54 describes the script version of the same thing: functionality that only a pointing-device event reaches. With `visibility: hidden` or `display: none`, the buttons are out of the Tab order. With `opacity: 0`, they are in it, but invisible: a keyboard user can press a button they can't see, which is no better. And a touch screen can't hover at all, so phone users get neither version.

When the revealed row covers other content, 1.4.13 Content on Hover or Focus applies too. Content that appears on hover and obscures what was there must be dismissible without moving the pointer or focus, usually with Escape. Actions that slide in over the price fail that, because nothing closes them.

## Who is affected

The pattern is common on product grids, file lists, photo galleries and dashboards, wherever a designer wants the resting state quiet. The same card often carries its only Save or Delete there.

## What automation and AI miss

The markup is fine: real buttons with names, inside the card. A static scan sees them and passes. The failure is in a CSS rule and in what never happens: nobody hovers, and nothing else shows the row. A tool has to hover a card, see the buttons appear, then focus them and see whether they are still there. When asked to fix it, AI coding tools can add `tabindex="0"` to the card, which puts the card in the Tab order but still shows nothing, or swap `visibility` for `opacity`, which moves the failure from one form to the other.

## Before: actions hidden until the card is hovered

```html
<article class="card">
  <a href="/tote"><img src="tote.jpg" alt="Linen tote, natural">Linen tote</a>
  <p class="price">$48</p>
  <div class="actions">
    <button type="button">Save</button>
    <button type="button">Add to bag</button>
  </div>
</article>
```

```css
.card .actions {
  visibility: hidden;
  opacity: 0;
  transition: opacity 0.15s;
}
.card:hover .actions {
  visibility: visible;
  opacity: 1;
}
```

Only `:hover` reveals the row. Hidden with `visibility: hidden`, the buttons are out of the Tab order, so the keyboard never reaches them, and a touch screen never hovers.

## Before: actions kept transparent, sliding in over the price

```css
.card {
  position: relative;
}
.card .actions {
  position: absolute;
  inset: auto 0 0;
  opacity: 0;
  translate: 0 100%;
  transition: opacity 0.15s, translate 0.15s;
}
.card:hover .actions {
  opacity: 1;
  translate: 0;
}
```

Tab reaches the buttons now, but they stay at `opacity: 0` while focused, so nothing on screen shows where focus is or what Enter would press. The row also covers the bottom of the card when shown, and no key dismisses it.

## After: shown on focus and on touch screens too, in their own row

```css
/* The row has its own space, so it never covers the photo or the price. */
.card .actions {
  visibility: hidden;
  opacity: 0;
  transition: opacity 0.15s;
}
/* Hover, and keyboard focus anywhere in the card, reveal it. */
.card:hover .actions,
.card:focus-within .actions {
  visibility: visible;
  opacity: 1;
}
/* No hover to rest on: show the actions all the time. */
@media (hover: none) {
  .card .actions {
    visibility: visible;
    opacity: 1;
  }
}
@media (prefers-reduced-motion: reduce) {
  .card .actions {
    transition: none;
  }
}
```

`:focus-within` matches the card while any element inside it has focus, so the row appears as soon as Tab lands on the card's link, and the buttons after it are visible when focus reaches them. `visibility` must flip in the same rule, or the buttons stay out of the Tab order. The `hover: none` media query is true on phones, and on tablets used by touch, where the row would otherwise never show. The row keeps its own place under the price, so it covers nothing when it appears and 1.4.13 doesn't ask for a way to dismiss it. The pointer reveal stays as it was, for everyone who has a pointer.

## After: always shown, drawn to fit

```html
<article class="card">
  <a href="/tote"><img src="tote.jpg" alt="Linen tote, natural">Linen tote</a>
  <p class="price">$48</p>
  <div class="actions">
    <button type="button" class="quiet">Save</button>
    <button type="button" class="quiet">Add to bag</button>
  </div>
</article>
```

```css
/* Small, outlined, in the card's own ink: present without competing with the photo. */
.card .actions {
  display: flex;
  gap: 0.5rem;
}
.card .quiet {
  min-height: 24px;
  padding: 0.25rem 0.75rem;
  border: 1px solid currentColor;
  border-radius: 999px;
  background: transparent;
  color: inherit;
  font-size: 0.875rem;
}
.card .quiet:hover,
.card .quiet:focus-visible {
  background: var(--ink);
  color: var(--paper);
}
```

Nothing is hidden, so there is nothing to reveal: the buttons are in the Tab order, visible when focused, present on a phone, and never cover anything. The quiet is designed instead of hidden: outlined pills in the card's own text colour, small, with a filled hover and focus state. The resting grid is nearly as calm as before, and every way of using it works.

## Which option to use

Both pass. The reveal on focus and touch keeps the design as drawn for pointer users and costs three CSS rules, but each new place the card appears needs the same rules, and the `hover: none` fallback means the phone layout looks different from the desktop one, which the design file should show. Always-shown actions change the resting look, and ask the designer to draw the buttons so that they belong there. For a card whose actions matter, such as Add to bag on a shop grid or Delete on a file list, draw them in: an action a shop depends on shouldn't rely on anyone finding a hover. Keep the reveal for secondary actions on a grid that would otherwise be crowded, with the focus and touch rules in place.

A third shape is an always-shown menu button on each card, with the actions in a menu it opens. It takes the least space of all, and the [right-click, long-press and swipe actions with no button](/learn/context-menus) guide shows the menu in full.

## In the design file

The reveal is a state, and a state needs drawing. Add a focus variant of the card beside the hover one, with the actions shown and the focus ring on the first button, and a touch variant with the actions always present, so that the developer has something to build the `:focus-within` and `hover: none` rules from. For the always-shown version, draw the buttons at their real size: 24 by 24 pixels is the minimum target under 2.5.8, and the outlined pill needs 3:1 against the card for its border and 4.5:1 for its text. Write the contrast ratios beside the swatches, so they survive a palette change.

## Implementation decisions

Give each button a name that says which product it acts on when the grid has many: a visually hidden span, "Save Linen tote", or `aria-describedby` pointing at the card's name, so a screen reader user moving through buttons alone isn't told "Save, Save, Save". The focus ring on a revealed button needs 3:1 against the card like any other; see [missing focus indicators on custom buttons](/learn/visible-focus). If the whole card is a link and the buttons sit inside it, nested interactive content is invalid HTML and reads badly: keep the link on the name and photo, and the buttons as siblings of it.

Under forced colors, `opacity` and `visibility` still apply, so a hidden row stays hidden there too; the fixes above work unchanged. A row shown with `display: none` and `display: flex` works the same way as `visibility` in the fixes, but can't be faded in; pick one and apply it to every card component.

## Verify the fix

1. Put the mouse aside and Tab through the grid: on each card, the actions are visible while focus is on the card or on one of them.
2. Press Enter or Space on Save and on Add to bag from the keyboard, and confirm each runs.
3. On a phone, or with the device toolbar set to a touch device, confirm the actions are visible without hovering and respond to a single tap.
4. At 400% zoom or under a magnifier, hover a card and confirm the actions cover nothing, or that Escape hides them with the pointer still on the card.
5. With a screen reader, move through the buttons and confirm each says which product it is for.

Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the card component or the grid's theme changes.

## Limits

This is a starting pattern, not a guarantee for every application. The reveal rules assume the actions are plain buttons inside the card; a library's card component may render them elsewhere in the DOM, where `:focus-within` won't see them. Test on your real grid, with a keyboard, a phone and a magnifier.

---

Get this guide in your coding agent: MCP https://easeweb.dev/mcp (setup at https://easeweb.dev/services/developers)
