CSS + JS Products & branding #loop #product #device #clock #time

Smartwatch in CSS + JavaScript

A watch with thickness, strap stubs and the real time. Once a second JS writes three angles and two integers; CSS turns the hands and prints the digits with counters.

  • 186 lines of CSS
  • 17 lines of HTML
  • 16 lines of JS
  • No dependencies
  • MIT licensed

How it works

  1. Once a second JS writes --h, --m, --s as angles. Each hand stands on the centre (bottom: 50%), turns about its foot and reads its own variable.
  2. The angles count from midnight, so they only ever grow: at 59 → 0 seconds the hand keeps going forward instead of sweeping back.
  3. The digits need no DOM writes: JS also sets two integers, counter-reset: hh var(--hh) mm var(--mm) turns them into counters and content: counter(hh, decimal-leading-zero) prints 09:05.
  4. The case is seven rounded slabs 1.5px apart (they make the rounded sides), with the face and back at ±5px. The straps are hinged on the case edge with transform-origin and tilted away with rotateX.

Key techniques

  • setInterval → --h / --m / --s angles
  • digits via counter-reset: var(--hh)
  • straps hinged on the case edge
  • rounded slabs for thickness

Copy-paste code

HTML 17 lines

<div class="scene">
  <div class="watch">
    <div class="body">
      <i class="strap top"></i><i class="strap bottom"></i>
      <i class="slab" style="--i:0"></i><i class="slab" style="--i:1"></i><i class="slab" style="--i:2"></i>
      <i class="slab mid" style="--i:3"></i>
      <i class="slab" style="--i:4"></i><i class="slab" style="--i:5"></i><i class="slab" style="--i:6"></i>
      <i class="crown"></i>
      <div class="back"></div>
      <div class="face">
        <div class="dial"><i class="hand h"></i><i class="hand m"></i><i class="hand s"></i></div>
        <div class="time"></div>
        <small>live time</small>
      </div>
    </div>
  </div>
</div>

CSS 186 lines

.scene {
  perspective: 800px;
}

.watch {
  --metal: #5a607f;
  --band: #6550ba;
  transform-style: preserve-3d;
  animation: sway 7s ease-in-out infinite alternate;
}

/* 88 × 104, 10px thick, 24px corners */
.body {
  position: relative;
  width: 88px;
  height: 104px;
  transform-style: preserve-3d;
}

/* hinged on the back edge of the case, then tilted away round an invisible wrist */
.strap {
  position: absolute;
  left: 16px;
  width: 56px;
  height: 44px;
  background:
    linear-gradient(90deg, rgb(0 0 0 / 0.35), transparent 22% 78%, rgb(0 0 0 / 0.35)),
    var(--band);
}

.strap.top {
  bottom: 100%;
  border-radius: 10px 10px 0 0;
  transform-origin: 50% 100%;
  transform: translateZ(-4px) rotateX(38deg);
}

.strap.bottom {
  top: 100%;
  border-radius: 0 0 10px 10px;
  background:
    radial-gradient(circle, rgb(0 0 0 / 0.45) 0 2px, transparent 2.5px) 50% 8px / 100% 10px repeat-y,
    linear-gradient(90deg, rgb(0 0 0 / 0.35), transparent 22% 78%, rgb(0 0 0 / 0.35)),
    var(--band);
  transform-origin: 50% 0;
  transform: translateZ(-4px) rotateX(-38deg);
}

/* rounded slabs fill the case, corners included */
.slab {
  position: absolute;
  inset: 0.5px;
  border-radius: 24px;
  background: var(--metal);
  transform: translateZ(calc((var(--i) - 3) * 1.5px));
}

.slab.mid {
  background: color-mix(in srgb, #eceefb 45%, var(--metal)); /* a polished band */
}

/* ribbed crown plate, plus its end face turned side-on */
.crown {
  position: absolute;
  top: 30px;
  left: calc(100% - 3px);
  width: 7px;
  height: 20px;
  border-radius: 2px;
  background: repeating-linear-gradient(var(--metal) 0 1.5px, #a3a7bd 1.5px 3px);
  transform-style: preserve-3d;
  transform: translateZ(0.75px);
}

.crown::before {
  content: '';
  position: absolute;
  top: 0;
  left: calc(100% - 4px);
  width: 8px;
  height: 100%;
  border-radius: 2px;
  background: inherit;
  transform: rotateY(90deg);
}

.back,
.face {
  position: absolute;
  inset: 0;
  box-sizing: border-box;
  border-radius: 24px;
  backface-visibility: hidden;
}

.back {
  background:
    radial-gradient(circle, #05060c 0 10px, #1e8c85 11px 13px, transparent 14px),
    radial-gradient(circle, #868ba4 0 30px, var(--metal) 31px);
  transform: rotateY(180deg) translateZ(5px);
}

/* black glass, the border is the bezel */
.face {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 9px 6px 6px;
  overflow: hidden;
  border: 5px solid #05060c;
  background: radial-gradient(circle at 50% 30%, #231c42, #05060c 70%);
  color: #fff;
  font-family: system-ui, sans-serif;
  transform: translateZ(5px);
}

.face::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(125deg, rgb(255 255 255 / 0.16), transparent 38%);
}

.face small {
  margin-top: 1px;
  color: #2ee6d6;
  font-size: 6px;
  font-weight: 700;
  letter-spacing: 1.2px;
  text-transform: uppercase;
}

/* 12 ticks from a repeating conic gradient, a dark disc on top, an activity arc behind */
.dial {
  position: relative;
  flex: none;
  width: 50px;
  height: 50px;
  border-radius: 50%;
  background:
    radial-gradient(circle, #0b0d18 0 19px, transparent 19.5px),
    repeating-conic-gradient(from -1.5deg, rgb(255 255 255 / 0.85) 0 3deg, transparent 3deg 30deg),
    conic-gradient(#ff4d9d 0 68%, rgb(255 77 157 / 0.25) 0);
  box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.12);
}

.dial::after {
  content: '';
  position: absolute;
  top: calc(50% - 2.5px);
  left: calc(50% - 2.5px);
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: #2ee6d6;
}

/* stands on the centre, turns about its foot */
.hand {
  position: absolute;
  bottom: 50%;
  border-radius: 2px;
  background: #fff;
  transform-origin: 50% 100%;
}

.hand.h { left: calc(50% - 1.5px); width: 3px; height: 12px; transform: rotate(var(--h, 300deg)); }
.hand.m { left: calc(50% - 1px); width: 2px; height: 18px; transform: rotate(var(--m, 54deg)); }
.hand.s { left: calc(50% - 0.5px); width: 1px; height: 21px; background: #2ee6d6; transform: rotate(var(--s, 180deg)); }

/* two integers from JS become counters; decimal-leading-zero pads them */
.time {
  margin-top: 5px;
  counter-reset: hh var(--hh, 10) mm var(--mm, 9);
  font: 700 15px/1 ui-monospace, Consolas, monospace;
  letter-spacing: -0.5px;
}

.time::before {
  content: counter(hh, decimal-leading-zero) ':' counter(mm, decimal-leading-zero);
}

@keyframes sway {
  from { transform: rotateX(10deg) rotateY(-30deg); }
  to   { transform: rotateX(6deg) rotateY(30deg); }
}

JS 16 lines

const watch = document.querySelector('.watch');

function tick() {
  const d = new Date();
  // seconds since midnight: the angles only grow, so no hand sweeps backwards at 59 → 0
  const t = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
  watch.style.setProperty('--s', t * 6 + 'deg');
  watch.style.setProperty('--m', (t / 10).toFixed(1) + 'deg');
  watch.style.setProperty('--h', (t / 120).toFixed(2) + 'deg');
  // integers for the digital readout (CSS counters)
  watch.style.setProperty('--hh', d.getHours());
  watch.style.setProperty('--mm', d.getMinutes());
}

tick();
setInterval(tick, 1000);

Sass source 216 lines

$w: 88px;
$h: 104px;
$d: 10px; // case thickness
$r: 24px; // corner radius

.d-watch {
  --metal: color-mix(in srgb, var(--muted) 55%, var(--surface-solid));
  --band: color-mix(in srgb, var(--accent) 70%, #0b0d18);
  transform-style: preserve-3d;
  animation: d-watch-sway 7s ease-in-out infinite alternate;

  &__body {
    position: relative;
    width: $w;
    height: $h;
    transform-style: preserve-3d;
  }

  // Straps hang off the BACK edge of the case and are hinged there (transform-origin on the
  // case side), then tilted away so they curve round an invisible wrist.
  &__strap {
    position: absolute;
    left: 16px;
    width: $w - 32px;
    height: 44px;
    background:
      linear-gradient(90deg, rgb(0 0 0 / 0.35), transparent 22% 78%, rgb(0 0 0 / 0.35)),
      var(--band);

    &--top {
      bottom: 100%;
      border-radius: 10px 10px 0 0;
      transform-origin: 50% 100%;
      transform: translateZ($d * -0.5 + 1px) rotateX(38deg);
    }

    &--bottom {
      top: 100%;
      border-radius: 0 0 10px 10px;
      // a row of pin holes down the middle
      background:
        radial-gradient(circle, rgb(0 0 0 / 0.45) 0 2px, transparent 2.5px) 50% 8px / 100% 10px repeat-y,
        linear-gradient(90deg, rgb(0 0 0 / 0.35), transparent 22% 78%, rgb(0 0 0 / 0.35)),
        var(--band);
      transform-origin: 50% 0;
      transform: translateZ($d * -0.5 + 1px) rotateX(-38deg);
    }
  }

  // No walls: seven rounded slabs 1.5px apart fill the case, corners included.
  &__slab {
    position: absolute;
    inset: 0.5px;
    border-radius: $r;
    background: var(--metal);
    transform: translateZ(calc((var(--i) - 3) * 1.5px));

    // the middle slab (--i: 3, after the two straps) is a touch brighter: a polished band
    &:nth-child(6) {
      background: color-mix(in srgb, var(--text) 45%, var(--metal));
    }
  }

  // the crown: a ribbed plate poking out of the right side, plus its end face turned side-on
  &__crown {
    position: absolute;
    top: 30px;
    left: calc(100% - 3px);
    width: 7px;
    height: 20px;
    border-radius: 2px;
    background: repeating-linear-gradient(var(--metal) 0 1.5px, color-mix(in srgb, var(--text) 50%, var(--metal)) 1.5px 3px);
    transform-style: preserve-3d;
    transform: translateZ(0.75px); // between two slabs, never coplanar with one

    &::before {
      content: '';
      position: absolute;
      top: 0;
      left: calc(100% - 4px);
      width: 8px;
      height: 100%;
      border-radius: 2px;
      background: inherit;
      transform: rotateY(90deg);
    }
  }

  &__back,
  &__face {
    position: absolute;
    inset: 0;
    border-radius: $r;
    backface-visibility: hidden;
  }

  &__back {
    background:
      radial-gradient(circle, #05060c 0 10px, color-mix(in srgb, var(--accent-2) 60%, #05060c) 11px 13px, transparent 14px),
      radial-gradient(circle, color-mix(in srgb, var(--text) 30%, var(--metal)) 0 30px, var(--metal) 31px);
    transform: rotateY(180deg) translateZ($d * 0.5);
  }

  // black glass with the bezel as padding
  &__face {
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 9px 6px 6px;
    overflow: hidden;
    border: 5px solid #05060c;
    background: radial-gradient(circle at 50% 30%, color-mix(in srgb, var(--accent) 22%, #05060c), #05060c 70%);
    color: #fff;
    transform: translateZ($d * 0.5);

    // glass reflection, fixed to the glass
    &::after {
      content: '';
      position: absolute;
      inset: 0;
      background: linear-gradient(125deg, rgb(255 255 255 / 0.16), transparent 38%);
      pointer-events: none;
    }

    small {
      margin-top: 1px;
      color: var(--accent-2);
      font-size: 6px;
      font-weight: 700;
      letter-spacing: 1.2px;
      text-transform: uppercase;
    }
  }

  // Twelve tick marks: a repeating conic gradient, covered in the middle by a dark disc.
  // An accent arc around it doubles as an activity ring.
  &__dial {
    position: relative;
    flex: none;
    width: 50px;
    height: 50px;
    border-radius: 50%;
    background:
      radial-gradient(circle, #0b0d18 0 19px, transparent 19.5px),
      repeating-conic-gradient(from -1.5deg, rgb(255 255 255 / 0.85) 0 3deg, transparent 3deg 30deg),
      conic-gradient(var(--hot) 0 68%, color-mix(in srgb, var(--hot) 25%, transparent) 0);
    box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.12);

    // centre cap, above the hands
    &::after {
      content: '';
      position: absolute;
      top: calc(50% - 2.5px);
      left: calc(50% - 2.5px);
      width: 5px;
      height: 5px;
      border-radius: 50%;
      background: var(--accent-2);
    }
  }

  // Each hand stands on the centre (bottom: 50%) and turns about its foot. The angles only
  // ever grow (JS counts from midnight), so no hand sweeps backwards when the minute rolls over.
  &__hand {
    position: absolute;
    bottom: 50%;
    border-radius: 2px;
    background: #fff;
    transform-origin: 50% 100%;

    &--h {
      left: calc(50% - 1.5px);
      width: 3px;
      height: 12px;
      transform: rotate(var(--h, 300deg));
    }

    &--m {
      left: calc(50% - 1px);
      width: 2px;
      height: 18px;
      transform: rotate(var(--m, 54deg));
    }

    &--s {
      left: calc(50% - 0.5px);
      width: 1px;
      height: 21px;
      background: var(--accent-2);
      transform: rotate(var(--s, 180deg));
    }
  }

  // Digits without touching the DOM: the two integers from JS become counters, and
  // `decimal-leading-zero` pads them to 09:05.
  &__time {
    margin-top: 5px;
    counter-reset: hh var(--hh, 10) mm var(--mm, 9);
    font: 700 15px/1 ui-monospace, 'SF Mono', Consolas, monospace;
    letter-spacing: -0.5px;

    &::before {
      content: counter(hh, decimal-leading-zero) ':' counter(mm, decimal-leading-zero);
    }
  }
}

@keyframes d-watch-sway {
  from {
    transform: rotateX(10deg) rotateY(-30deg);
  }

  to {
    transform: rotateX(6deg) rotateY(30deg);
  }
}
Standalone snippet · plain CSS, no build step, no dependencies.