AI CSS Animations

Flip Clock Animation

A flip clock whose seconds cards fold down every second, top half falling to reveal the next digit. The digits come from a registered integer and a cyclic @counter-style, so 9 wraps to 0.

Also known as: flip clock css, flip counter, split flap clock.

1 0

HTML

<div class="txt-flip-clock" aria-label="Flip clock">
  <span class="txt-flip-clock-card">1</span>
  <span class="txt-flip-clock-card">0</span>
  <span class="txt-flip-clock-colon" aria-hidden="true"></span>
  <span class="txt-flip-clock-card txt-flip-clock-tens" style="--n: 6; --p: 10s; --at: -58s" aria-hidden="true"><i></i><i></i><i></i><i></i></span>
  <span class="txt-flip-clock-card txt-flip-clock-ones" style="--n: 10; --p: 1s; --at: -7s" aria-hidden="true"><i></i><i></i><i></i><i></i></span>
</div>

CSS

@counter-style txt-flip-clock-ten {
  system: cyclic;
  symbols: "0" "1" "2" "3" "4" "5" "6" "7" "8" "9";
}
@counter-style txt-flip-clock-six {
  system: cyclic;
  symbols: "0" "1" "2" "3" "4" "5";
}
@property --txt-flip-clock-d {
  syntax: "<integer>";
  initial-value: 0;
  inherits: false;
}
.txt-flip-clock {
  --c: var(--txt-color, #3b82f6);
  font-size: var(--txt-size, 32px);
  font-family: system-ui, sans-serif;
  font-weight: 700;
  line-height: 1.2;
  display: inline-flex;
  align-items: center;
  gap: 0.1em;}
.txt-flip-clock-card {
  position: relative;
  width: 0.74em;
  height: 1.12em;
  font-size: 1.9em;
  line-height: 1.12em;
  text-align: center;
  font-variant-numeric: tabular-nums;
  color: #f8fafc;
  background: color-mix(in srgb, var(--c) 22%, #111827);
  border-radius: 0.1em;
  perspective: 2.5em;
  box-shadow: 0 0.05em 0.12em rgb(0 0 0 / 0.3);
}
/* the hinge line across the middle */
.txt-flip-clock-card::before {
  content: "";
  position: absolute;
  inset: calc(50% - 0.02em) 0 auto;
  height: 0.04em;
  background: rgb(0 0 0 / 0.45);
  z-index: 2;
}
.txt-flip-clock-colon {
  width: 0.24em;
  height: 0.95em;
  background:
    radial-gradient(circle closest-side, var(--c) 92%, transparent) top / 0.24em 0.24em no-repeat,
    radial-gradient(circle closest-side, var(--c) 92%, transparent) bottom / 0.24em 0.24em no-repeat;
}
/* four layers: static top (next), static bottom (current),
   falling top flap (current), landing bottom flap (next) */
.txt-flip-clock-card i {
  position: absolute;
  inset: 0;
  font-style: normal;
  border-radius: inherit;
  background: inherit;
  backface-visibility: hidden;
}
.txt-flip-clock-card i:nth-child(odd) { clip-path: inset(0 0 50% 0); }
.txt-flip-clock-card i:nth-child(even) { clip-path: inset(50% 0 0 0); }
.txt-flip-clock-tens,
.txt-flip-clock-ones {
  counter-reset: txt-flip-clock-cur calc(var(--txt-flip-clock-d) + 1) txt-flip-clock-nxt calc(var(--txt-flip-clock-d) + 2);
  animation: txt-flip-clock-count calc(var(--p) * var(--n)) steps(var(--n)) var(--at) infinite;
}
.txt-flip-clock-ones i:is(:nth-child(1), :nth-child(4))::after { content: counter(txt-flip-clock-nxt, txt-flip-clock-ten); }
.txt-flip-clock-ones i:is(:nth-child(2), :nth-child(3))::after { content: counter(txt-flip-clock-cur, txt-flip-clock-ten); }
.txt-flip-clock-tens i:is(:nth-child(1), :nth-child(4))::after { content: counter(txt-flip-clock-nxt, txt-flip-clock-six); }
.txt-flip-clock-tens i:is(:nth-child(2), :nth-child(3))::after { content: counter(txt-flip-clock-cur, txt-flip-clock-six); }
.txt-flip-clock-card i:nth-child(3) { z-index: 1; }
.txt-flip-clock-card i:nth-child(4) { z-index: 1; transform: rotateX(90deg); }
.txt-flip-clock-ones i:nth-child(3) { animation: txt-flip-clock-fall 1s var(--at) infinite; }
.txt-flip-clock-ones i:nth-child(4) { animation: txt-flip-clock-land 1s var(--at) infinite; }
.txt-flip-clock-tens i:nth-child(3) { animation: txt-flip-clock-fall-slow 10s var(--at) infinite; }
.txt-flip-clock-tens i:nth-child(4) { animation: txt-flip-clock-land-slow 10s var(--at) infinite; }
@keyframes txt-flip-clock-count {
  to { --txt-flip-clock-d: var(--n); }
}
/* each flip takes 0.6s: 0.3s for the top to fall, 0.3s for the bottom to land */
@keyframes txt-flip-clock-fall {
  0%       { transform: rotateX(0); filter: brightness(1); animation-timing-function: cubic-bezier(0.55, 0, 0.9, 0.5); }
  30%, 100% { transform: rotateX(-90deg); filter: brightness(0.55); }
}
@keyframes txt-flip-clock-land {
  0%, 30%  { transform: rotateX(90deg); filter: brightness(0.55); animation-timing-function: cubic-bezier(0.2, 0.8, 0.35, 1.25); }
  60%, 100% { transform: rotateX(0); filter: brightness(1); }
}
@keyframes txt-flip-clock-fall-slow {
  0%      { transform: rotateX(0); filter: brightness(1); animation-timing-function: cubic-bezier(0.55, 0, 0.9, 0.5); }
  3%, 100% { transform: rotateX(-90deg); filter: brightness(0.55); }
}
@keyframes txt-flip-clock-land-slow {
  0%, 3%  { transform: rotateX(90deg); filter: brightness(0.55); animation-timing-function: cubic-bezier(0.2, 0.8, 0.35, 1.25); }
  6%, 100% { transform: rotateX(0); filter: brightness(1); }
}

@media (prefers-reduced-motion: reduce) {
  .txt-flip-clock, .txt-flip-clock *, .txt-flip-clock::before, .txt-flip-clock::after, .txt-flip-clock *::before, .txt-flip-clock *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

Customizing this snippet

  • Color: set --txt-color on the element or a parent. Currently previewing #3b82f6.
  • Font size: set --txt-size. Every other dimension is in em, so the whole thing scales together. Currently previewing 40px.
  • Speed: change the duration on the transition or animation shorthand. Staggered delays are relative, so they keep their rhythm.
  • Reduced motion: the rule at the end of the CSS turns the motion off for people who asked their OS for less of it. Keep it.

Similar text animations