Search code examples
htmlcsscss-specificity

Transition time is only applied if target value changes


Note: I updated the question with a simpler example as I found that it is not an issue with transform in particular, but with how the transition timing is applied. You can still see the more complex example in the edit history.

Here is a simple example: when you mouse over the "Grow" button, the bar will grow to 400px in 5 seconds; and after exiting the "Grow" button, the bar will shrink automatically to 0px in 10 seconds. If you mouse over the "Shrink" button, the bar will shrink to 0px in 2 seconds.

Although it doesn't, it takes 10 seconds to go back to 0px even when mousing over "Shrink":

#bar {
  width:0;
  border-left:1px solid gray;
  height: 40px;
  transition: width 10s;
  background: green;
}

#grow,#shrink {
  display:inline-block;
  width: 60px;
  height: 60px;
  background: gray;
  margin-bottom: 10px;
  color: white;
  line-height: 60px;
  text-align: center;
  box-shadow: inset 0 0 0 1px black;
  margin-right: 20px;
}

#grow:hover ~ #bar {
  width: 400px;
  transition: width 5s;
}

#shrink:hover ~ #bar {
  width: 0px;
  transition: width 2s;
}
<span id="grow">Grow</span>
<span id="shrink">Shrink</span>

<div id="bar"></div>

At first I thought it could be a problem with specificity (even when #shrink:hover ~ #bar is more specific than just #bar), so I added an !important like this:

#shrink:hover ~ #bar {
  width: 0px;
  transition: width 2s !important;
}

Still it didn't work. After some try-error, I found that changing the value actually makes the browser take the new transition time, even if the change is insignificant. So, for example, this will work:

#bar {
  width:0;
  border-left:1px solid gray;
  height: 40px;
  transition: width 10s;
  background: green;
}

#grow,#shrink {
  display:inline-block;
  width: 60px;
  height: 60px;
  background: gray;
  margin-bottom: 10px;
  color: white;
  line-height: 60px;
  text-align: center;
  box-shadow: inset 0 0 0 1px black;
  margin-right: 20px;
}

#grow:hover ~ #bar {
  width: 400px;
  transition: width 5s;
}

#shrink:hover ~ #bar {
  width: 0.000001px;
  transition: width 2s;
}
<span id="grow">Grow</span>
<span id="shrink">Shrink</span>

<div id="bar"></div>

That works. Also, I noticed another case in which it works: when it goes directly from "Grow" to "Shrink" without transitioning to anything else (you may be able to see it by moving from "Grow" to "Shrink" really fast or by removing the margin so they are together).

I have been able to replicate this issue in Chrome, Internet Explorer/Edge, Firefox, and Safari. So I don't know if it's a real issue or me misunderstanding how this should work (quite possible too).

Why does this happen? Is there a way to apply the new transition time without having to change the target value?


Solution

  • I picked up this from the specs., saying

    For each element with a before-change style and an after-change style, and each property (other than shorthands) for which the before-change style is different from the after-change style, implementations must start transitions based on the relevant item

    which I interpret being the case here, where your #bar before-change value is not different from the #shrink:hover ~ #bar after-change value, hence no transition starts.

    Src: https://www.w3.org/TR/css3-transitions/#starting