Skip to the content

CSS hover effects for buttons, with focus-visible for keyboards

Last updated: By Lukas BrandtLesson 8 of 214 min read

A button that does not react when you point at it feels broken. CSS handles that feedback with pseudo-classes: keywords after a colon that match an element only while it is in a certain state. :hover matches while a mouse pointer is over the element. :focus-visible matches when the element has focus and the browser decides the focus should be shown, which in practice means when someone reaches it with the Tab key. This lesson styles both together, because a hover effect that keyboard users never see leaves them guessing where they are on the page.

The code

Add one line to .btn, then three new rules after .btn-light:

.btn {
  /* ...lessons 2 to 7 as before... */
  /* Lesson 8: hover feedback */
  cursor: pointer;
}

/* Lesson 8: hover and keyboard focus */
.btn:hover,
.btn:focus-visible {
  background-color: #1e40af;
  border-color: #1e40af;
}

.btn-light:hover,
.btn-light:focus-visible {
  color: #1e40af;
  background-color: #dbeafe;
}

.btn:focus-visible {
  outline: 3px solid #b45309;
  outline-offset: 2px;
}

What each rule does

cursor: pointer shows the hand cursor. Links get it by default, while buttons in most browsers show the normal arrow. Setting it on .btn makes all three consistent.

The first rule uses a selector list: two selectors separated by a comma share one declaration block. On hover or keyboard focus, the background and border turn a darker blue, #1e40af. Changing only border-color keeps the width and style from lesson 4. White text on the darker blue has a contrast ratio of about 8.7:1, even better than the resting state.

The second rule rescues the light variant. Without it, the light button would also get the dark blue background from the first rule while keeping its blue text, which would be nearly unreadable. .btn:hover and .btn-light:hover have the same specificity (one class plus one pseudo-class each), so the rule written later wins. The result is a pale blue fill, #dbeafe, with dark blue text at about 7.2:1. Its border turns dark blue too, from the first rule.

The third rule draws the focus ring. An outline takes no space, so the ring appears without moving anything, and outline-offset: 2px leaves a small gap so the ring reads as separate from the button's own border. The amber brown #b45309 stands out against the blue buttons and has a contrast ratio of about 5:1 against the white page. WCAG asks for at least 3:1 for a visible focus indicator.

Why focus-visible and not focus

:focus matches whenever an element has focus, including right after a mouse click. That is why some sites show a ring on click, someone finds it ugly, and all focus styles get removed, which locks keyboard users out. :focus-visible follows the browser's own judgment: a mouse click on a button generally shows no ring, a Tab press does, and a text field shows focus either way. Keyboard users get a clear ring, mouse users get no flash. MDN lists it as available in all major browsers since March 2022.

Test it now. Click an empty part of the page, then press Tab. Focus moves to the first link, then the second, then the button, and each gets the darker fill and the amber ring. Shift+Tab walks back.

Common mistakes

  • A space before the colon. .btn :hover with a space means "any hovered element inside a .btn", which matches nothing here. Write .btn:hover.
  • Removing the outline without a replacement. outline: none on focus is one of the most common accessibility bugs on the web. If you restyle focus, replace the ring, do not delete it.
  • Hover-only information. Touch screens have no hover worth relying on: a tap may trigger :hover briefly or not at all. Use hover for feedback, never to reveal something the reader needs.
  • Rules in the wrong order. If .btn-light:hover sits above .btn:hover, the general rule wins and the light button turns dark blue with blue text on hover.
Parts links can earn us a commission. As an Amazon Associate I earn from qualifying purchases.

Questions

Should buttons get :visited and :active styles too?

A visited state rarely makes sense on a button, and .btn sets its own text color, so the browser's purple never shows. If you style plain text links, write the states in the order :link, :visited, :hover, :active. They share the same specificity, so a later state would otherwise override an earlier one at the wrong moment.

Can the color fade instead of switching?

Yes, with a transition on .btn. It tells the browser to animate a property between its old and new value instead of switching at once:

/* Optional experiment, not part of the project file */
.btn {
  transition: background-color 0.15s ease, border-color 0.15s ease;
}

Because the transition sits on .btn and not on the hover rule, the fade runs both ways, in and out. We leave it out of the project file to keep Project 1 to the properties these lessons cover, but it is a good experiment.

Try it: in the online HTML editor, paste your file, press Tab through the three buttons, then hover each with the mouse. If anything differs from the description above, compare your rule order with the code block at the top of this page.