Open demo

CSS code

HTML
<div><span class="lbl">Split-flap digits</span>
<div class="flip" data-flip><span class="sr-only" data-say>Current time</span>
  <div class="grp" aria-hidden="true"><div class="d" data-digit>0</div><div class="d" data-digit>0</div></div><span class="sep" aria-hidden="true">:</span>
  <div class="grp" aria-hidden="true"><div class="d" data-digit>0</div><div class="d" data-digit>0</div></div><span class="sep" aria-hidden="true">:</span>
  <div class="grp" aria-hidden="true"><div class="d" data-digit>0</div><div class="d" data-digit>0</div></div>
</div></div>
CSS
/* Split-flap clock: six digit cards with a seam across the middle. The script writes each [data-digit]
   and sets data-flipping on the cards that changed, which plays a short fold. */
.flip{display:flex;gap:12px;align-items:center}
.flip .grp{display:flex;gap:5px}
.flip .d{position:relative;width:62px;height:88px;background:#1e293b;border-radius:9px;color:#fff;
  font-size:3rem;font-weight:600;display:grid;place-items:center;font-variant-numeric:tabular-nums;
  box-shadow:0 3px 10px rgba(15,23,42,.28);overflow:hidden}
.flip .d::after{content:"";position:absolute;left:0;right:0;top:50%;height:1px;background:rgba(0,0,0,.45)}
.flip .d[data-flipping]{animation:flip-fold .32s ease-in-out}
.flip .sep{font-size:2.1rem;font-weight:700;color:#7c8aa0}
@keyframes flip-fold{0%{transform:rotateX(0)}50%{transform:rotateX(-88deg)}100%{transform:rotateX(0)}}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
@media (max-width:520px){.flip{gap:8px}.flip .grp{gap:4px}.flip .d{width:40px;height:60px;font-size:2rem;border-radius:7px}.flip .sep{font-size:1.5rem}}
@media (prefers-reduced-motion:reduce){.flip .d[data-flipping]{animation:none}}
JavaScript
// Flip clock. Writes HHMMSS into the six [data-digit] cards of each [data-flip] root once a second and
// restarts the fold only on cards whose digit changed. Reduced motion turns the fold off in CSS;
// the digits still update. The visually hidden [data-say] text changes once a minute.
(function () {
  var roots = document.querySelectorAll('[data-flip]');
  if (!roots.length) return;
  var lastMinute = -1;
  function p2(n) { return String(n).padStart(2, '0'); }
  function tick() {
    var d = new Date(), digits = p2(d.getHours()) + p2(d.getMinutes()) + p2(d.getSeconds());
    var newMinute = d.getMinutes() !== lastMinute; lastMinute = d.getMinutes();
    roots.forEach(function (root) {
      root.querySelectorAll('[data-digit]').forEach(function (card, i) {
        if (card.textContent === digits[i]) return;
        card.textContent = digits[i];
        card.removeAttribute('data-flipping');
        void card.offsetWidth; // restart the animation
        card.setAttribute('data-flipping', '');
      });
      var say = root.querySelector('[data-say]');
      if (say && newMinute) say.textContent = 'Current time ' + digits.slice(0, 2) + ':' + digits.slice(2, 4);
    });
    setTimeout(tick, 1000 - Date.now() % 1000);
  }
  tick();
})();

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Split-flap digits</span>
<div class="flip d-flex align-items-center" data-flip><span class="visually-hidden" data-say>Current time</span>
  <div class="grp d-flex" aria-hidden="true"><div class="flip-card position-relative d-grid text-bg-dark fw-semibold rounded overflow-hidden" data-digit>0</div><div class="flip-card position-relative d-grid text-bg-dark fw-semibold rounded overflow-hidden" data-digit>0</div></div><span class="flip-sep fw-bold" aria-hidden="true">:</span>
  <div class="grp d-flex" aria-hidden="true"><div class="flip-card position-relative d-grid text-bg-dark fw-semibold rounded overflow-hidden" data-digit>0</div><div class="flip-card position-relative d-grid text-bg-dark fw-semibold rounded overflow-hidden" data-digit>0</div></div><span class="flip-sep fw-bold" aria-hidden="true">:</span>
  <div class="grp d-flex" aria-hidden="true"><div class="flip-card position-relative d-grid text-bg-dark fw-semibold rounded overflow-hidden" data-digit>0</div><div class="flip-card position-relative d-grid text-bg-dark fw-semibold rounded overflow-hidden" data-digit>0</div></div>
</div></div>
CSS
/* Split-flap clock on Bootstrap utilities: .d-flex / .gap-* lay out the cards, .text-bg-dark (recoloured through
   --bs-dark-rgb) and .position-relative build each card. The seam and the fold are the only custom CSS. */
.flip{--bs-dark-rgb:30,41,59;--bs-border-radius:9px;gap:12px}
.flip .grp{gap:5px}
.flip-card{width:62px;height:88px;place-items:center;font-size:3rem;font-variant-numeric:tabular-nums;line-height:1;
  box-shadow:0 3px 10px rgba(15,23,42,.28)}
.flip-card::after{content:"";position:absolute;left:0;right:0;top:50%;height:1px;background:rgba(0,0,0,.45)}
.flip-card[data-flipping]{animation:flip-fold .32s ease-in-out}
.flip-sep{font-size:2.1rem;color:#7c8aa0}
@keyframes flip-fold{0%{transform:rotateX(0)}50%{transform:rotateX(-88deg)}100%{transform:rotateX(0)}}
@media (max-width:520px){.flip{--bs-border-radius:7px;gap:8px}.flip .grp{gap:4px}.flip-card{width:40px;height:60px;font-size:2rem}.flip-sep{font-size:1.5rem}}
@media (prefers-reduced-motion:reduce){.flip-card[data-flipping]{animation:none}}
JavaScript
// Flip clock. Writes HHMMSS into the six [data-digit] cards of each [data-flip] root once a second and
// restarts the fold only on cards whose digit changed. Reduced motion turns the fold off in CSS;
// the digits still update. The visually hidden [data-say] text changes once a minute.
(function () {
  var roots = document.querySelectorAll('[data-flip]');
  if (!roots.length) return;
  var lastMinute = -1;
  function p2(n) { return String(n).padStart(2, '0'); }
  function tick() {
    var d = new Date(), digits = p2(d.getHours()) + p2(d.getMinutes()) + p2(d.getSeconds());
    var newMinute = d.getMinutes() !== lastMinute; lastMinute = d.getMinutes();
    roots.forEach(function (root) {
      root.querySelectorAll('[data-digit]').forEach(function (card, i) {
        if (card.textContent === digits[i]) return;
        card.textContent = digits[i];
        card.removeAttribute('data-flipping');
        void card.offsetWidth; // restart the animation
        card.setAttribute('data-flipping', '');
      });
      var say = root.querySelector('[data-say]');
      if (say && newMinute) say.textContent = 'Current time ' + digits.slice(0, 2) + ':' + digits.slice(2, 4);
    });
    setTimeout(tick, 1000 - Date.now() % 1000);
  }
  tick();
})();

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#64748b] uppercase">Split-flap digits</span>
<div class="flex items-center gap-2 min-[521px]:gap-3" data-flip><span class="sr-only" data-say>Current time</span>
  <div class="flex gap-1 min-[521px]:gap-[5px]" aria-hidden="true"><div class="relative grid h-[60px] w-10 place-items-center overflow-hidden rounded-[7px] bg-[#1e293b] text-[2rem] font-semibold text-white tabular-nums shadow-[0_3px_10px_rgba(15,23,42,.28)] after:absolute after:inset-x-0 after:top-1/2 after:h-px after:bg-black/45 data-flipping:animate-flip-fold motion-reduce:animate-none min-[521px]:h-[88px] min-[521px]:w-[62px] min-[521px]:rounded-[9px] min-[521px]:text-[3rem]" data-digit>0</div><div class="relative grid h-[60px] w-10 place-items-center overflow-hidden rounded-[7px] bg-[#1e293b] text-[2rem] font-semibold text-white tabular-nums shadow-[0_3px_10px_rgba(15,23,42,.28)] after:absolute after:inset-x-0 after:top-1/2 after:h-px after:bg-black/45 data-flipping:animate-flip-fold motion-reduce:animate-none min-[521px]:h-[88px] min-[521px]:w-[62px] min-[521px]:rounded-[9px] min-[521px]:text-[3rem]" data-digit>0</div></div><span class="text-[1.5rem] font-bold text-[#7c8aa0] min-[521px]:text-[2.1rem]" aria-hidden="true">:</span>
  <div class="flex gap-1 min-[521px]:gap-[5px]" aria-hidden="true"><div class="relative grid h-[60px] w-10 place-items-center overflow-hidden rounded-[7px] bg-[#1e293b] text-[2rem] font-semibold text-white tabular-nums shadow-[0_3px_10px_rgba(15,23,42,.28)] after:absolute after:inset-x-0 after:top-1/2 after:h-px after:bg-black/45 data-flipping:animate-flip-fold motion-reduce:animate-none min-[521px]:h-[88px] min-[521px]:w-[62px] min-[521px]:rounded-[9px] min-[521px]:text-[3rem]" data-digit>0</div><div class="relative grid h-[60px] w-10 place-items-center overflow-hidden rounded-[7px] bg-[#1e293b] text-[2rem] font-semibold text-white tabular-nums shadow-[0_3px_10px_rgba(15,23,42,.28)] after:absolute after:inset-x-0 after:top-1/2 after:h-px after:bg-black/45 data-flipping:animate-flip-fold motion-reduce:animate-none min-[521px]:h-[88px] min-[521px]:w-[62px] min-[521px]:rounded-[9px] min-[521px]:text-[3rem]" data-digit>0</div></div><span class="text-[1.5rem] font-bold text-[#7c8aa0] min-[521px]:text-[2.1rem]" aria-hidden="true">:</span>
  <div class="flex gap-1 min-[521px]:gap-[5px]" aria-hidden="true"><div class="relative grid h-[60px] w-10 place-items-center overflow-hidden rounded-[7px] bg-[#1e293b] text-[2rem] font-semibold text-white tabular-nums shadow-[0_3px_10px_rgba(15,23,42,.28)] after:absolute after:inset-x-0 after:top-1/2 after:h-px after:bg-black/45 data-flipping:animate-flip-fold motion-reduce:animate-none min-[521px]:h-[88px] min-[521px]:w-[62px] min-[521px]:rounded-[9px] min-[521px]:text-[3rem]" data-digit>0</div><div class="relative grid h-[60px] w-10 place-items-center overflow-hidden rounded-[7px] bg-[#1e293b] text-[2rem] font-semibold text-white tabular-nums shadow-[0_3px_10px_rgba(15,23,42,.28)] after:absolute after:inset-x-0 after:top-1/2 after:h-px after:bg-black/45 data-flipping:animate-flip-fold motion-reduce:animate-none min-[521px]:h-[88px] min-[521px]:w-[62px] min-[521px]:rounded-[9px] min-[521px]:text-[3rem]" data-digit>0</div></div>
</div></div>
CSS
@theme {
  --animate-flip-fold: flip-fold .32s ease-in-out;
  @keyframes flip-fold { 0% { transform: rotateX(0) } 50% { transform: rotateX(-88deg) } 100% { transform: rotateX(0) } }
}
JavaScript
// Flip clock. Writes HHMMSS into the six [data-digit] cards of each [data-flip] root once a second and
// restarts the fold only on cards whose digit changed. Reduced motion turns the fold off in CSS;
// the digits still update. The visually hidden [data-say] text changes once a minute.
(function () {
  var roots = document.querySelectorAll('[data-flip]');
  if (!roots.length) return;
  var lastMinute = -1;
  function p2(n) { return String(n).padStart(2, '0'); }
  function tick() {
    var d = new Date(), digits = p2(d.getHours()) + p2(d.getMinutes()) + p2(d.getSeconds());
    var newMinute = d.getMinutes() !== lastMinute; lastMinute = d.getMinutes();
    roots.forEach(function (root) {
      root.querySelectorAll('[data-digit]').forEach(function (card, i) {
        if (card.textContent === digits[i]) return;
        card.textContent = digits[i];
        card.removeAttribute('data-flipping');
        void card.offsetWidth; // restart the animation
        card.setAttribute('data-flipping', '');
      });
      var say = root.querySelector('[data-say]');
      if (say && newMinute) say.textContent = 'Current time ' + digits.slice(0, 2) + ':' + digits.slice(2, 4);
    });
    setTimeout(tick, 1000 - Date.now() % 1000);
  }
  tick();
})();

About this set

A split-flap style clock in six dark digit cards, grouped as hours, minutes and seconds with colon separators. Each card has a thin seam across the middle like a mechanical flap display, and when its digit changes the card folds away and back in a 320 millisecond animation. Only the cards whose digit actually changed flip, so most seconds move just one card and the clock stays calm to look at. Use it on a landing page, an event screen or a product that wants a tactile, retro feel; for a dense dashboard the plain digital set is quieter. The script writes the six digits of the current time into the [data-digit] cards once a second, re-timed to the second boundary, and restarts the animation by removing and re-adding a data-flipping attribute, which the CSS turns into the fold. Below 520 pixels the cards shrink so all six fit a phone screen without scrolling. The Bootstrap version builds the cards from display, colour and rounding utilities, and the Tailwind version plays the same keyframes through a data-flipping variant.

What’s included

  • Six digit cards with a centre seam, grouped as HH:MM:SS
  • Only cards whose digit changed play the fold
  • Animation triggered by a data-flipping attribute, no class juggling
  • Cards shrink below 520 pixels so the clock fits a phone
  • Fold switched off under prefers-reduced-motion while the digits keep updating

Accessibility

The cards and separators are aria-hidden, and a visually hidden line such as "Current time 21:48" changes once a minute without being a live region, so a screen reader is never told every second. Under prefers-reduced-motion the fold animation is removed and the digits simply change. The colon separators reach 3:1 contrast as large text.

Customise it

Change the card background, size and font size on .flip .d in plain CSS, and the fold speed in the flip-fold animation. In Bootstrap recolour the cards through --bs-dark-rgb and --bs-border-radius on .flip; in Tailwind edit the bg-, h-, w- and text- utilities and the --animate-flip-fold theme value.