7

I'm developing a flip animation to show new numbers; it's much like an analog clock or calendar with the hinge in the middle.

The approach is straight forward: have a div with:

  • The bottom half of the first number on one side
  • The top half of the second number rotated 180 degrees so it's on the back

In order to show the new number, I rotate that whole div around the center of the container, revealing the back of the rotating div: Number flip animation in latest Firefox

However, in Chrome, the animation doesn't always work. Sometimes half disappears completely until the transition animation is complete and sometimes the old number doesn't render: Number flip animation in latest Chrome with the bottom of the number not appearing till after animation is complete

In Safari 12, it's worse, it doesn't seem to respect backface-visibility, even with the -webkit- prefix: Safari 12 Number animation, the bottom half of the first number is inverted after animation is complete

Pre-Chromium Edge handles this fine, but new (checked in v83) Edge has the same issue as Chrome.

I've tried messing around with the properties and have looked through other backface-visibility questions here.

Here's the code, hover over the numbers to see the flip:

body {
  background: #2e517d;
}

.container {
  width: 175px;
  height: 192px;
  background: #4e9bfa;
  position: relative;
  left: 50%;
  transform: translate(-50%, 50%);
  perspective: 1000px;
}

.cover {
  width: 175px;
  height: 50%;
  position: absolute;
  top: 96px;
  background-color: #34b58c;
  transform: rotateX(0deg);
  transform-style: preserve-3d;
  transform-origin: top;
  transition: all 0.5s ease-out;
}

.container:hover .cover {
  transform: rotateX(180deg);
}

.flip {
  margin: 0;
  display: block;
  position: absolute;
  width: 100%;
  height: 100%;
  backface-visibility: hidden;
}

.container p {
  font-size: 1000%;
  margin: 0;
}

.container>p {
  height: 96px;
  overflow: hidden;
}

.front-number-bottom {
  position: relative;
  height: 96px;
  overflow: hidden;
  background-color: red;
}

.front-number-bottom p {
  margin: 0;
  position: relative;
  top: -96px;
}

.back-number-top {
  position: relative;
  height: 96px;
  overflow: hidden;
}

.back-number-bottom {
  height: 96px;
  overflow: hidden;
  position: relative;
  z-index: -1;
}

.back-number-bottom p {
  margin: 0;
  position: relative;
  top: -96px;
}

div.front {
  background: red;
}

div.back {
  background: green;
  transform: rotateX(180deg);
}
<body>
  <div class="container">
    <p>76</p>
    <div id="cover" class="cover">
      <div class="flip front">
        <div class="front-number-bottom">
          <p>76</p>
        </div>
      </div>
      <div class="flip back">
        <div class="back-number-top">
          <p>77</p>
        </div>
      </div>
    </div>
    <div class="back-number-bottom">
      <p>77</p>
    </div>
  </div>
  </div>
</body>

Is this a sound approach that can be easily fixed in Chromium browsers and Safari?

Would a different approach be better?

  • Have you tried with `z-index`? – Leo Jun 23 '20 at 22:56
  • @Leo, how were you envisioning using `z-index`? I haven't tried manipulating it during animation. – Nicholas Weber Jun 24 '20 at 17:17
  • Doing something similar ( [this](https://i.imgur.com/eNMZC50.gifv) ), I learned that when rotating elements generates unpredictable behaviors for the content rendering, specially for it's `z-index`. In these cases, we should set that index manually instead of letting it render whenever it wants. – Leo Jun 25 '20 at 16:42

1 Answers1

1

I guess your code is a bit complex. I would simplify your logic like below where you no more need backface-visibility: hidden;

Note the usage of two important things:

  • the mask that allow me to cut the element and show only 50% of the height (top or bottom). This will make the animation more realistic since each number will have both top and bottom part seperated.
  • the z-index trick where I apply a transtion that change the z-index exactly at the middle of the animation (when the rotations are at 90deg)1.

.card {
  width: 175px;
  height: 192px;
  position: relative;
  z-index:0;
  left: 50%;
  transform: translate(-50%, 50%);
  font-size: 160px;
}
.card span,
.card span::before,
.card span::after {
  position:absolute;
  top:0;
  left:0;
  right:0;
  bottom:0;
}
.card span {
  position:absolute;
  z-index:2;
  perspective: 1000px;
}
.card span:first-child {
  z-index:3;
  transition:0s 0.25s all linear;
}
.card span::before,
.card span::after{
  content:attr(data-number);
  -webkit-mask:linear-gradient(#fff,#fff) top/100% 50% no-repeat;
          mask:linear-gradient(#fff,#fff) top/100% 50% no-repeat;
  background:red;
  transition:0.5s all linear;
  transform-style: preserve-3d;
}
.card span::after {
  -webkit-mask-position:bottom;
          mask-position:bottom;
  background:green;
}

.card span:first-child::after {
    transform: rotateX(0deg);
}
.card span:last-child::before {
    transform: rotateX(-180deg);
}

/* Hover */
.card:hover span:first-child {
  z-index:1;
}
.card:hover span:first-child::after {
    transform: rotateX(180deg);
}
.card:hover span:last-child::before {
    transform: rotateX(0deg);
}
<div class="card">
  <span data-number="76"></span>
  <span data-number="77"></span>
</div>

The mask can be replaced with clip-path too:

.card {
  width: 175px;
  height: 192px;
  position: relative;
  z-index:0;
  left: 50%;
  transform: translate(-50%, 50%);
  font-size: 160px;
}
.card span,
.card span::before,
.card span::after {
  position:absolute;
  top:0;
  left:0;
  right:0;
  bottom:0;
}
.card span {
  z-index:2;
  perspective: 1000px;
}
.card span:first-child {
  z-index:3;
  transition:0s 0.25s all linear;
}
.card span::before,
.card span::after{
  content:attr(data-number);
  clip-path:polygon(0 0,100% 0,100% 50%,0 50%);
  background:red;
  transition:0.5s all linear;
  transform-style: preserve-3d;
}
.card span::after {
  clip-path:polygon(0 50%,100% 50%,100% 100%,0 100%);
  background:green;
}

.card span:first-child::after {
    transform: rotateX(0deg);
}
.card span:last-child::before {
    transform: rotateX(-180deg);
}

/* Hover */
.card:hover span:first-child {
  z-index:1;
}
.card:hover span:first-child::after {
    transform: rotateX(180deg);
}
.card:hover span:last-child::before {
    transform: rotateX(0deg);
}
<div class="card">
  <span data-number="76"></span>
  <span data-number="77"></span>
</div>

Another optimization using counter and without setting an explicit width/height

.card {
  margin:0 5px;
  font-family:monospace;
  display:inline-block;
  text-align:center;
  position: relative;
  z-index:0;
  font-size: 150px;
  counter-reset:num calc(var(--n,1) - 1);
}
/* this will defined the height/width*/
.card::after {
  content:counter(num);
  visibility:hidden;
}
/**/
.card span,
.card span::before,
.card span::after {
  position:absolute;
  top:0;
  left:0;
  right:0;
  bottom:0;
}
.card span {
  z-index:2;
  perspective: 1000px;
  counter-increment:num;
}
.card span:first-child {
  z-index:3;
  transition:0s 0.25s all linear;
}
.card span::before,
.card span::after{
  content:counter(num);
  clip-path:polygon(0 0,100% 0,100% 50%,0 50%);
  background:red;
  transition:0.5s all linear;
  transform-style: preserve-3d;
}
.card span::after {
  clip-path:polygon(0 50%,100% 50%,100% 100%,0 100%);
  background:green;
}

.card span:first-child::after,
.card:hover span:last-child::before{
    transform: rotateX(0deg);
}
.card span:last-child::before {
    transform: rotateX(-180deg);
}
.card:hover span:first-child::after {
    transform: rotateX(180deg);
}
.card:hover span:first-child {
  z-index:1;
}
<div class="card" style="--n:75">
  <span></span><span></span>
</div>

<div class="card" style="--n:5">
  <span></span><span></span>
</div>

<div class="card" style="--n:100">
  <span></span><span></span>
</div>

1 When using linear it's pretty easy but it's more trick with other ease functions. Here is a related question that can help you identify the middfle of ease functions: When exactly does an ease animation reach its midpoint?

Temani Afif
  • 245,468
  • 26
  • 309
  • 415
  • These flicker if I change the transition-timing-function to something other than linear (ease-out, for instance). What's the best way around that? – Nicholas Weber Jun 24 '20 at 17:56
  • 1
    @NicholasWeber read this: https://stackoverflow.com/a/62474609/8620333 you need to make sure the z-index update exactly at the middle of your animation so you need to find the time when it's at the middle. For linear it's easy because it's 50% but for ease it's different – Temani Afif Jun 24 '20 at 19:17