Search code examples
cssreactjscss-selectorsstyled-componentspseudo-class

applying the same selectors on sibling have a slightly different behaviour


In my app, I have a component in which there are only two cards and I want to animate them when they are hovered and clicked. I noticed that when one of them is currently active (clicked) the other one still can be hovered so I wrote this:

 // apply the hover styles based on this condition => my data-active="true" AND I don't have any sibling AFTER-ME(~)  AND BEFORE-ME(+) which has data-activ="true"
  &[data-active="true"],
  &:hover:not(:has(+ [data-active="true"])),
  &:hover:not(:has(~ [data-active="true"])){
    flex: 2 auto;
  }

but this has a bug, which is when the first card in the DOM is active=true and the latter in the DOM is active=false, this latter STILL can be hovered but not vice-versa, the result is actually satisfying but not as perfect as I want it, what I care about the most now is to know why the latter/second card behaves differently?!

What I tried:

1- I tried to separate the conditions to ensure they were not conflicting with each other but got the same behaviour.

2- I thought there was a missing condition that may the one caused this bug but none of what I came up with solved it. maybe I still missing something, I don't know.

3- tried to make each card a <button> element so that I could make use of the disabled but couldn't change it from disabled=false to true and vice-versa from JavaScript.

here is a video to give a clearer idea of how the behaviour is different, the active card has a pink background:

css-selector-bug-video

The Full Code: Card component (the onClick function is to handle the dataset.active):

<CardsBox>
    <OfferCard onClick={handleClick} heading="Front-End Services"/>
    <OfferCard onClick={handleClick} heading="Back-End Services"/>
</CardsBox>

OfferCard component:

const Card = styled(motion.button)`
  max-height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  backdrop-filter: blur(2.5rem) saturate(0%);
  -webkit-backdrop-filter: blur(2.5rem) saturate(0%);
  background-color: rgba(176, 123, 255, 0.63);
  border-radius: var(--md-radius);
  box-shadow: 0px 0px 0.5rem var(--neon-purple);
  padding: 3rem;
  flex: 1;
  transition: all 0.8s cubic-bezier(0.19, 1, 0.22, 1),
    flex 0.8s cubic-bezier(0.19, 1, 0.22, 1);

  cursor: pointer;

  h4 {
    font-size: var(--secondary-heading);
    font-weight: 400;

    transition: all 0.2s ease-in-out;
  }

  &[data-active="true"],
  &:hover:not(:has(+ [data-active="true"])),
  &:hover:not(:has(~ [data-active="true"])){
    flex: 2 auto;

    h4 {
      font-weight: 700;
    }
  }
}

export default function OfferCard({ onClick, heading, children }) {
      return (
         <Card id="offer-card" data-active="false" onClick={onClick}></Card>
    )
   }

Solution

  • Working through what those three selectors do,

    &[data-active="true"]
    

    says, “Match me when I have [data-active="true"]. Simple enough.

    &:hover:not(:has(+ [data-active="true"]))
    

    says, “Match me when I’m hovered and when my immediate next sibling doesn’t have [data-active="true"].

    &:hover:not(:has(~ [data-active="true"]))
    

    is very similar. It says, “Match me when I’m hovered and when any subsequent sibling doesn’t have [data-active="true"].

    Based on the behavior you’re going for, you may be mistaking the ~ subsequent sibling combinator for something like an any sibling combinator. However, no such thing exists.

    You can go about this by checking the parent to make sure it doesn’t have any children with the data attribute, and only then apply the hover.

    &[data-active="true"],
    :not(:has([data-active="true"])) > &:hover
    

    .boxes {
      margin-block: 8px;
        height: 80px;
        display: flex;
        gap: 20px;
    }
    
    .box {
        background-color: royalblue;
        border-radius: 20px;
        flex: 1 auto;
        transition: flex-grow 200ms;
    
        &[data-active="true"],
        :not(:has([data-active="true"])) > &:hover {
            flex: 2 auto;
        }
    }
    <div class="boxes">
        <div class="box"></div>
        <div class="box"></div>
    </div>
    
    <div class="boxes">
        <div class="box" data-active="true"></div>
        <div class="box"></div>
    </div>
    
    <div class="boxes">
        <div class="box"></div>
        <div class="box" data-active="true"></div>
    </div>