Pure CSS Products & branding #hover #loop #product #device #music #cylinder

Headphones in pure CSS

Over-ear headphones turning on a stand: the band is a ring of tangent strips, each cup a short cylinder laid on its side. Hover or focus and they turn to face you and light up.

  • 222 lines of CSS
  • 58 lines of HTML
  • No JavaScript
  • No dependencies
  • Snippet under MIT
GitHub

How it works

  1. The band is twelve flat strips. Each one starts at the arc centre, turns to its angle with rotateZ, moves out by the radius with translateY(-58 units) and folds flat with rotateX(90deg): together they are the top surface of an arch. Two masked rings close its sides.
  2. A cup is a short cylinder built the easy way, standing up like a can (strips at rotateY(i × 30deg) translateZ(r), a disc on each end), then laid on its side with rotateZ(±90deg) so the top disc faces outward.
  3. Light is computed, not painted by hand: each strip darkens itself with cos() / sin() of its own angle, so the strips facing up are the brightest.
  4. The turn cannot simply be switched off on hover: removing an animation snaps back. Instead two @property values animate: the lap angle --spin and a weight --k. The transform uses --spin × --k, and hover eases --k to 0, which glides from wherever the lap is to the front pose.
  5. The hovered element is a static wrapper and everything that moves has pointer-events: none, so the hover never flickers.
  6. Every length is a multiple of one base unit, --u, tied to the canvas, so the headset is the same share of a gallery card, the editor and a recording canvas.

Key techniques

  • arc = strips at rotateZ(a) translateY(−r) rotateX(90deg)
  • cylinder built upright, laid down with rotateZ(90deg)
  • shading from sin() / cos() of the strip angle
  • @property blend: hover eases --k from 1 to 0

Copy-paste code

HTML 58 lines

<div class="scene">
  <div class="headphones" tabindex="0" role="img" aria-label="Headphones that turn and light up on hover or focus">
   <div class="rig">
    <i class="shadow"></i>
    <div class="band">
      <b class="side"></b><b class="side"></b>
      <i style="--i:0"></i>
      <i style="--i:1"></i>
      <i style="--i:2"></i>
      <i style="--i:3"></i>
      <i style="--i:4"></i>
      <i style="--i:5"></i>
      <i style="--i:6"></i>
      <i style="--i:7"></i>
      <i style="--i:8"></i>
      <i style="--i:9"></i>
      <i style="--i:10"></i>
      <i style="--i:11"></i>
    </div>
    <i class="slider slider-l"></i><i class="slider slider-lf"></i>
    <i class="yoke yoke-l"></i>
    <div class="cup cup-l">
      <i style="--i:0"></i>
      <i style="--i:1"></i>
      <i style="--i:2"></i>
      <i style="--i:3"></i>
      <i style="--i:4"></i>
      <i style="--i:5"></i>
      <i style="--i:6"></i>
      <i style="--i:7"></i>
      <i style="--i:8"></i>
      <i style="--i:9"></i>
      <i style="--i:10"></i>
      <i style="--i:11"></i>
      <b class="outer"><em></em></b>
      <b class="cushion"></b>
    </div>
    <i class="slider slider-r"></i><i class="slider slider-rf"></i>
    <i class="yoke yoke-r"></i>
    <div class="cup cup-r">
      <i style="--i:0"></i>
      <i style="--i:1"></i>
      <i style="--i:2"></i>
      <i style="--i:3"></i>
      <i style="--i:4"></i>
      <i style="--i:5"></i>
      <i style="--i:6"></i>
      <i style="--i:7"></i>
      <i style="--i:8"></i>
      <i style="--i:9"></i>
      <i style="--i:10"></i>
      <i style="--i:11"></i>
      <b class="outer"><em></em></b>
      <b class="cushion"></b>
    </div>
   </div>
  </div>
</div>

CSS 222 lines

/* registered, so they can be animated and transitioned */
@property --spin {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}

@property --k {
  syntax: '<number>';
  inherits: false;
  initial-value: 1;
}

.scene {
  /* one base unit: every length below is a multiple of it, so the headset is the same share of a
     card, the editor, a full screen and a recording canvas */
  --u: 0.34vmin;
  perspective: calc(800 * var(--u));
}

/* the static hit area */
.headphones {
  --shell: color-mix(in srgb, #8b6cff 34%, #17182a);
  --shell-hi: color-mix(in srgb, #8b6cff 55%, #3a3d5c);
  --leather: color-mix(in srgb, #8b6cff 14%, #1b1c29);
  --leather-hi: color-mix(in srgb, #8b6cff 22%, #3a3c52);
  --stitch: color-mix(in srgb, #8b6cff 80%, #fff);
  --pad: #16171f;
  --metal: #b8bdd2;
  --metal-dark: #5c6280;
  --glow: #2ee6d6;
  display: grid;
  place-items: center;
  width: calc(216 * var(--u));
  height: calc(184 * var(--u));
  border-radius: calc(16 * var(--u));
  cursor: pointer;
  transform-style: preserve-3d;
}

/* 0 × 0: every part is placed around this point */
.rig {
  position: relative;
  width: 0;
  height: 0;
  pointer-events: none;
  transform-style: preserve-3d;
  /* the front pose (-28deg) plus the lap, weighted by --k */
  transform: rotateX(-14deg) rotateY(calc(-28deg + var(--spin) * var(--k)));
  animation: turn 16s linear infinite;
  transition: --k 1.1s cubic-bezier(0.45, 0, 0.2, 1);
}

.headphones:hover .rig,
.headphones:focus-visible .rig {
  --k: 0; /* the lap keeps running, it just stops counting */
}

.shadow {
  position: absolute;
  left: calc(-80 * var(--u));
  top: calc(34 * var(--u));
  width: calc(160 * var(--u));
  height: calc(80 * var(--u));
  border-radius: 50%;
  background: radial-gradient(closest-side, rgb(0 0 0 / 0.45), transparent);
  transform: rotateX(90deg);
}

/* --- the band: 12 strips over 200deg, radius 58 units --- */
.band {
  position: absolute;
  top: calc(-22 * var(--u));
  transform-style: preserve-3d;
}

.band i {
  --a: calc(-100deg + (var(--i) + 0.5) * 16.667deg);
  position: absolute;
  left: calc(-8.8 * var(--u));
  top: calc(-11 * var(--u));
  width: calc(17.6 * var(--u)); /* one side of the polygon, a hair wider: no seams */
  height: calc(22 * var(--u));  /* the band's width, front to back */
  background:
    linear-gradient(rgb(0 0 0 / calc(0.5 - 0.46 * cos(var(--a)))) 0 0), /* shade: none on top */
    repeating-linear-gradient(90deg, var(--stitch) 0 calc(2 * var(--u)), transparent calc(2 * var(--u)) calc(4 * var(--u))) 0 calc(3 * var(--u)) / 100% calc(1 * var(--u)) no-repeat,
    repeating-linear-gradient(90deg, var(--stitch) 0 calc(2 * var(--u)), transparent calc(2 * var(--u)) calc(4 * var(--u))) 0 calc(18 * var(--u)) / 100% calc(1 * var(--u)) no-repeat,
    linear-gradient(var(--leather), var(--leather-hi) 45%, var(--leather));
  transform: rotateZ(var(--a)) translateY(calc(-58 * var(--u))) rotateX(90deg);
}

/* the band's side walls: a ring, masked to the same 200deg */
.side {
  position: absolute;
  left: calc(-58 * var(--u));
  top: calc(-58 * var(--u));
  width: calc(116 * var(--u));
  height: calc(116 * var(--u));
  background: radial-gradient(closest-side, transparent calc(100% - 6 * var(--u)), var(--leather) calc(100% - 5.5 * var(--u)), color-mix(in srgb, var(--leather) 70%, #000) 100%, transparent 100%);
  mask: conic-gradient(from -100deg, #000 0 200deg, transparent 0);
  transform: translateZ(calc(11 * var(--u)));
}

.side + .side {
  transform: translateZ(calc(-11 * var(--u)));
}

/* --- metal: sliders (two crossed planes) and yokes (half rings) --- */
.slider {
  position: absolute;
  top: calc(-18 * var(--u));
  left: calc(-5 * var(--u));
  width: calc(10 * var(--u));
  height: calc(20 * var(--u));
  background: linear-gradient(90deg, var(--metal-dark), var(--metal) 45%, #eef0fa 55%, var(--metal-dark));
}

.slider-l { transform: translateX(calc(-55 * var(--u))) rotateY(90deg); }
.slider-r { transform: translateX(calc(55 * var(--u))) rotateY(90deg); }
.slider-lf { left: calc(-1.5 * var(--u)); width: calc(3 * var(--u)); transform: translateX(calc(-55 * var(--u))); }
.slider-rf { left: calc(-1.5 * var(--u)); width: calc(3 * var(--u)); transform: translateX(calc(55 * var(--u))); }

.yoke {
  position: absolute;
  left: calc(-35 * var(--u));
  top: 0;
  width: calc(70 * var(--u));
  height: calc(70 * var(--u));
  background: radial-gradient(closest-side, transparent calc(100% - 4 * var(--u)), var(--metal) calc(100% - 3.5 * var(--u)), var(--metal-dark) 100%, transparent 100%);
  mask: conic-gradient(from -90deg, #000 0 180deg, transparent 0);
}

.yoke-l { transform: translateX(calc(-55 * var(--u))) rotateY(90deg); }
.yoke-r { transform: translateX(calc(55 * var(--u))) rotateY(90deg); }

/* --- cups: built standing (axis Y), then laid down; top disc = outside --- */
.cup {
  --dir: 1; /* which way "up" went: flips the light */
  position: absolute;
  top: calc(35 * var(--u));
  transform-style: preserve-3d;
  transform: translateX(calc(55 * var(--u))) rotateZ(90deg);
}

.cup-l {
  --dir: -1;
  transform: translateX(calc(-55 * var(--u))) rotateZ(-90deg);
}

/* 12 strips, radius 31 units, 26 units thick: 17 units shell, a seam, 9 units cushion */
.cup i {
  position: absolute;
  left: calc(-8.6 * var(--u));
  top: calc(-13 * var(--u));
  width: calc(17.2 * var(--u));
  height: calc(26 * var(--u));
  backface-visibility: hidden;
  background:
    linear-gradient(rgb(0 0 0 / calc(0.3 + 0.28 * var(--dir) * sin(var(--i) * 30deg))) 0 0),
    linear-gradient(var(--shell-hi) 0 calc(2 * var(--u)), var(--shell) calc(4 * var(--u)) calc(15 * var(--u)), var(--shell-hi) calc(16.5 * var(--u)), transparent calc(16.5 * var(--u))),
    linear-gradient(transparent calc(17 * var(--u)), #2a2c3c calc(18 * var(--u)), var(--pad) calc(21 * var(--u)), #0b0c12);
  transform: rotateY(calc(var(--i) * 30deg)) translateZ(calc(31 * var(--u)));
}

/* the seam light: its own layer, so hover only changes opacity */
.cup i::after {
  content: '';
  position: absolute;
  inset: calc(16 * var(--u)) 0 auto;
  height: calc(1.5 * var(--u));
  background: var(--glow);
  opacity: 0.25;
  transition: opacity 0.5s;
}

.outer,
.cushion {
  position: absolute;
  left: calc(-31.5 * var(--u));
  top: calc(-31.5 * var(--u));
  width: calc(63 * var(--u));
  height: calc(63 * var(--u));
  border-radius: 50%;
  backface-visibility: hidden;
}

.outer {
  background:
    radial-gradient(circle, var(--metal) 0 calc(2.5 * var(--u)), transparent calc(3 * var(--u))),
    radial-gradient(circle, transparent 0 calc(7 * var(--u)), var(--metal) calc(7.5 * var(--u)) calc(8.5 * var(--u)), transparent calc(9 * var(--u))),
    radial-gradient(circle, transparent 0 83%, var(--metal-dark) 85%, #eef0fa 90%, var(--metal) 94%, var(--metal-dark)),
    radial-gradient(circle at 35% 30%, var(--shell-hi), var(--shell) 60%, color-mix(in srgb, var(--shell) 60%, #000));
  transform: rotateX(90deg) translateZ(calc(13 * var(--u)));
}

.outer em {
  position: absolute;
  inset: calc(9 * var(--u));
  border: calc(2 * var(--u)) solid var(--glow);
  border-radius: 50%;
  box-shadow: 0 0 calc(8 * var(--u)) var(--glow), inset 0 0 calc(8 * var(--u)) var(--glow);
  opacity: 0.2;
  transition: opacity 0.5s;
}

.cushion {
  background: radial-gradient(circle, #06070b 0 52%, #2a2c3c 60%, var(--pad) 72%, #2a2c3c 86%, #0b0c12);
  transform: rotateX(-90deg) translateZ(calc(13 * var(--u)));
}

.headphones:hover .outer em,
.headphones:focus-visible .outer em,
.headphones:hover .cup i::after,
.headphones:focus-visible .cup i::after {
  opacity: 1;
}

/* -180 → 180: blending back to the pose never takes more than half a turn */
@keyframes turn {
  from { --spin: -180deg; }
  to   { --spin: 180deg; }
}
Standalone snippet · plain CSS, no build step, no dependencies.