How to change another element when hovering in CSS?
Style a child, a sibling, a parent or any element when another one is hovered, using CSS combinators and :has(), with keyboard, touch and JavaScript options.
CSS can change another element on hover if that element is a child or a later sibling of the hovered one. With :has(), it can also reach parents and previous siblings.
/* A child */
.card:hover .card-title {
color: #2563eb;
}
/* The next sibling */
.trigger:hover + .panel {
opacity: 1;
}
/* Any later sibling with the same parent */
.trigger:hover ~ .panel {
opacity: 1;
}+ matches only the element directly after the hovered one, and ~ matches any sibling after it. Neither goes backwards or outside the shared parent. That's why .panel:hover ~ .trigger does nothing when .trigger comes first in the HTML.
Parents and previous siblings with :has()
:has() styles an element based on what's inside it or after it, and it works in all major browsers. More examples are in is there a CSS parent selector.
/* The parent, when its button is hovered */
.card:has(.button:hover) {
box-shadow: 0 8px 24px rgb(0 0 0 / 0.12);
}
/* The previous sibling */
li:has(+ li:hover) {
opacity: 0.8;
}
/* Anywhere on the page */
body:has(.menu:hover) .overlay {
opacity: 1;
}A practical one: dim every menu link except the one under the pointer.
.menu:has(a:hover) a:not(:hover) {
opacity: 0.5;
}Use the body:has() pattern sparingly. Broad :has() selectors on body can make style recalculation more expensive on large pages, and they connect distant parts of the page in a way that's hard to track down later.
Keyboard users
Hover needs a pointer. If the effect reveals something useful, show it on focus too:
.card:is(:hover, :focus-within) .card-actions {
opacity: 1;
}
.trigger:is(:hover, :focus-visible) + .panel {
opacity: 1;
}:focus-within matches the card when anything inside it has focus, so the actions appear when a keyboard user tabs into the card. :focus-visible matches keyboard focus on the element itself.
Touch screens
Phones don't have real hover. A tap often triggers :hover, and it stays until the user taps somewhere else, which looks like a stuck state. Wrap hover-only effects in a media query:
@media (hover: hover) {
.card:hover .card-title {
color: #2563eb;
}
}Don't hide important content or actions behind hover alone, because touch users may never see them.
When you need JavaScript
If the two elements aren't related in the DOM and :has() would be awkward, toggle a class:
const button = document.querySelector('.cart-button');
const preview = document.querySelector('.cart-preview');
button.addEventListener('mouseenter', () => {
preview.classList.add('is-visible');
});
button.addEventListener('mouseleave', () => {
preview.classList.remove('is-visible');
});mouseenter and mouseleave don't fire again when the pointer moves between child elements, unlike mouseover and mouseout. Add focus and blur listeners for keyboard users as well. For more on toggling classes, see how to change an element's class with JavaScript.