Pure CSS Products & branding #loop #product #device #cylinder #photo

Retro camera in pure CSS

A rangefinder camera rocking on the table: a box body, a lens barrel of sixteen strips around the Z axis and a glass disc whose reflection slides the other way as it turns.

  • 249 lines of CSS
  • 35 lines of HTML
  • No JavaScript
  • No dependencies
  • MIT licensed

How it works

  1. The body is a plain box of six faces. The front carries the look: one gradient paints the silver top plate, the leatherette band and the bottom plate; a tiny dot pattern gives the leather its grain.
  2. The lens barrel is a cylinder around the Z axis: sixteen strips, each turned with rotateZ(i × 22.5deg), moved out by the radius, folded flat with rotateX(90deg) and slid forward by half its length so the barrel starts at the body.
  3. Each strip shades itself with cos() of its angle, so the barrel is lit from the top left without any hand-painted gradient per strip.
  4. The glass is a flat disc, so it can clip its reflection with overflow: hidden. The reflection runs the rocking animation in reverse (same duration, same easing), so it seems to stay with the room while the camera turns.
  5. The shutter button and the dial are flat discs laid on the top with rotateX(90deg), stacked 1.5px apart: from above they read as short cylinders.

Key techniques

  • barrel strips: rotateZ(a) translateY(−r) rotateX(90deg)
  • shading from cos() of the strip angle
  • reflection counter-moves in a clipped disc
  • stacked flat discs for knobs

Copy-paste code

HTML 35 lines

<div class="scene">
  <div class="camera">
    <i class="shadow"></i>
    <div class="face front"><i class="window"></i><i class="window"></i><span class="name">OBSCURA</span></div>
    <i class="face back"></i>
    <i class="face left"></i>
    <i class="face right"></i>
    <i class="face top"></i>
    <i class="face bottom"></i>
    <div class="lens">
      <i class="mount"></i>
      <i class="strip" style="--i:0"></i>
      <i class="strip" style="--i:1"></i>
      <i class="strip" style="--i:2"></i>
      <i class="strip" style="--i:3"></i>
      <i class="strip" style="--i:4"></i>
      <i class="strip" style="--i:5"></i>
      <i class="strip" style="--i:6"></i>
      <i class="strip" style="--i:7"></i>
      <i class="strip" style="--i:8"></i>
      <i class="strip" style="--i:9"></i>
      <i class="strip" style="--i:10"></i>
      <i class="strip" style="--i:11"></i>
      <i class="strip" style="--i:12"></i>
      <i class="strip" style="--i:13"></i>
      <i class="strip" style="--i:14"></i>
      <i class="strip" style="--i:15"></i>
      <i class="glass"><em></em></i>
      <i class="ring"></i>
    </div>
    <div class="flash"><i></i><i></i><i></i><i></i><i></i></div>
    <i class="dial" style="--h:1"></i><i class="dial dial-top" style="--h:4"></i>
    <i class="knob" style="--h:1"></i><i class="knob" style="--h:2.5"></i><i class="knob" style="--h:4"></i><i class="knob knob-top" style="--h:5.5"></i>
  </div>
</div>

CSS 249 lines

.scene {
  perspective: 800px;
}

/* 144 × 88, 40px deep */
.camera {
  --silver: #d9dce8;
  --silver-dark: #8a90a8;
  --leather: #2d2847;
  position: relative;
  width: 144px;
  height: 88px;
  transform-style: preserve-3d;
  animation: rock 7s ease-in-out infinite alternate;
}

.shadow {
  position: absolute;
  left: -16px;
  top: 48px;
  width: 176px;
  height: 80px;
  border-radius: 50%;
  background: radial-gradient(closest-side, rgb(0 0 0 / 0.42), transparent);
  transform: rotateX(90deg);
}

/* --- the body --- */
.face {
  position: absolute;
  top: 0;
  left: 0;
  width: 144px;
  height: 88px;
  /* silver plate, a seam, leatherette with grain, a seam, silver plate */
  background:
    linear-gradient(transparent 24px, rgb(0 0 0 / 0.35) 24px 25px, transparent 25px 79px, rgb(255 255 255 / 0.5) 79px 80px, transparent 80px),
    radial-gradient(circle, rgb(255 255 255 / 0.07) 0.8px, transparent 1.2px) 0 0 / 3px 3px,
    linear-gradient(var(--silver) 0 3px, #f4f5fa 8px, var(--silver) 18px, var(--silver-dark) 24px, var(--leather) 24px 80px, var(--silver) 80px, var(--silver-dark));
}

.front { transform: translateZ(20px); }
.back  { transform: rotateY(180deg) translateZ(20px); }
.left, .right { left: 52px; width: 40px; }
.left  { transform: rotateY(-90deg) translateZ(72px); }
.right { transform: rotateY(90deg) translateZ(72px); }
.left::after, .right::after { content: ''; position: absolute; inset: 0; background: rgb(0 0 0 / 0.3); }
.left::after { background: rgb(0 0 0 / 0.12); }

.top, .bottom { top: 24px; height: 40px; }
.top {
  background:
    repeating-linear-gradient(90deg, rgb(255 255 255 / 0.18) 0 1px, transparent 1px 3px),
    linear-gradient(var(--silver-dark), var(--silver) 20%, #f4f5fa 60%, var(--silver));
  transform: rotateX(90deg) translateZ(44px);
}
.bottom {
  background: linear-gradient(var(--silver-dark), #5b6078);
  transform: rotateX(-90deg) translateZ(44px);
}

.window {
  position: absolute;
  top: 6px;
  left: 11px;
  box-sizing: border-box;
  width: 26px;
  height: 13px;
  border: 1.5px solid var(--silver-dark);
  border-radius: 2px;
  background:
    linear-gradient(120deg, transparent 30%, rgb(255 255 255 / 0.45) 40%, transparent 50%),
    linear-gradient(160deg, #1a4a5a, #05060c);
}

.window + .window { left: auto; right: 11px; width: 14px; }

.name {
  position: absolute;
  top: 8px;
  width: 100%;
  color: #4a4f66;
  font: 800 7px system-ui, sans-serif;
  letter-spacing: 2.5px;
  text-align: center;
  text-indent: 2.5px;
  text-shadow: 0 1px 0 rgb(255 255 255 / 0.7); /* engraved */
}

/* --- the lens: a 0 × 0 anchor at its centre, on the front face --- */
.lens {
  position: absolute;
  left: 72px;
  top: 52px;
  transform-style: preserve-3d;
  transform: translateZ(20px);
}

.mount {
  position: absolute;
  left: -31px;
  top: -31px;
  width: 62px;
  height: 62px;
  border-radius: 50%;
  background: radial-gradient(circle, #0b0c12 0 76%, var(--silver-dark) 80%, #f4f5fa 88%, var(--silver) 94%, var(--silver-dark));
  transform: translateZ(0.5px);
}

/* 16 strips, radius 25px, 26px long. Strip top = at the body, bottom = the front. */
.strip {
  --a: calc(var(--i) * 22.5deg);
  position: absolute;
  left: -5.3px;
  top: -13px;
  width: 10.6px;
  height: 26px;
  backface-visibility: hidden;
  background:
    linear-gradient(rgb(0 0 0 / calc(0.36 - 0.34 * cos(var(--a) + 35deg))) 0 0), /* light from top left */
    linear-gradient(transparent 0 5px, rgb(0 0 0 / 0.5) 5px 6px, transparent 6px 17px, #ff4d9d 17px 18px, transparent 18px),
    repeating-linear-gradient(90deg, #2a2c3a 0 1.5px, #0d0e14 1.5px 3px) 0 6px / 100% 11px no-repeat, /* knurled grip */
    linear-gradient(#1a1b24 0 17px, var(--silver) 17px, #f4f5fa 22px, var(--silver-dark));
  transform: rotateZ(var(--a)) translateY(-25px) rotateX(90deg) translateY(13px);
}

.ring,
.glass {
  position: absolute;
  border-radius: 50%;
}

.ring {
  left: -25.5px;
  top: -25.5px;
  width: 51px;
  height: 51px;
  background: radial-gradient(circle, transparent 0 17px, #05060a 17.5px 18.5px, var(--silver-dark) 19px, #f4f5fa 21.5px, var(--silver) 23px, var(--silver-dark));
  transform: translateZ(26px);
}

.glass {
  left: -18px;
  top: -18px;
  width: 36px;
  height: 36px;
  overflow: hidden; /* flat disc: clipping is safe */
  background:
    radial-gradient(circle, #05060a 0 4px, transparent 5px),
    radial-gradient(circle, transparent 0 9px, rgb(139 108 255 / 0.5) 10px, transparent 12px),
    conic-gradient(from 200deg, #183a4e, #0a0c18 25%, #463a8a 50%, #0a0c18 75%, #183a4e);
  transform: translateZ(22px); /* recessed 4px behind the ring */
}

/* the reflection of a window: runs the rocking in reverse */
.glass em {
  position: absolute;
  top: 5px;
  left: 9px;
  width: 12px;
  height: 16px;
  border-radius: 50% 50% 40% 40%;
  background:
    linear-gradient(90deg, transparent 45%, rgb(0 0 0 / 0.5) 45% 55%, transparent 55%),
    linear-gradient(transparent 45%, rgb(0 0 0 / 0.5) 45% 55%, transparent 55%),
    rgb(255 255 255 / 0.55);
  animation: glare 7s ease-in-out infinite alternate;
}

/* --- on top --- */
.flash {
  position: absolute;
  top: -20px;
  left: 12px;
  width: 40px;
  height: 20px;
  transform-style: preserve-3d;
}

.flash i {
  position: absolute;
  inset: 0;
  background: linear-gradient(var(--silver), var(--silver-dark));
}

.flash i:nth-child(1) {
  box-sizing: border-box;
  border: 2px solid var(--silver-dark);
  background:
    repeating-linear-gradient(transparent 0 1.5px, rgb(0 0 0 / 0.08) 1.5px 2.5px), /* Fresnel lines */
    radial-gradient(ellipse at 50% 40%, #fff, #e7e9f3 55%, #b9bed0);
  transform: translateZ(12px);
}
.flash i:nth-child(2) { transform: rotateY(180deg) translateZ(12px); }
.flash i:nth-child(3),
.flash i:nth-child(4) { left: 8px; width: 24px; background: linear-gradient(var(--silver-dark), #5b6078); }
.flash i:nth-child(3) { transform: rotateY(90deg) translateZ(20px); }
.flash i:nth-child(4) { transform: rotateY(-90deg) translateZ(20px); }
.flash i:nth-child(5) {
  top: -2px;
  height: 24px;
  background: linear-gradient(90deg, var(--silver), #f4f5fa 50%, var(--silver));
  transform: rotateX(90deg);
}

/* flat discs centred on the top edge, lifted by --h, laid down */
.knob,
.dial {
  position: absolute;
  border-radius: 50%;
  transform: translateY(calc(var(--h) * -1px)) rotateX(90deg);
}

.knob {
  left: 110px;
  top: -8px;
  width: 16px;
  height: 16px;
  background: radial-gradient(circle, var(--silver) 0 45%, var(--silver-dark));
}

.knob-top {
  background: radial-gradient(circle at 40% 35%, #fff 0 8%, #ff4d9d 35%, #8c2a57);
}

.dial {
  left: 74px;
  top: -12px;
  width: 24px;
  height: 24px;
  background: repeating-conic-gradient(#5b6078 0 5deg, var(--silver) 5deg 10deg); /* knurled edge */
}

.dial-top {
  background:
    linear-gradient(transparent 46%, #ff4d9d 46% 54%, transparent 54%) 50% 0 / 2px 50% no-repeat,
    radial-gradient(circle, #f4f5fa, var(--silver) 60%, var(--silver-dark) 64%, transparent 66%),
    repeating-conic-gradient(#5b6078 0 5deg, var(--silver) 5deg 10deg);
}

@keyframes rock {
  from { transform: rotateX(-14deg) rotateY(-30deg); }
  to   { transform: rotateX(-8deg) rotateY(26deg); }
}

@keyframes glare {
  from { transform: translateX(10px) rotate(-12deg); }
  to   { transform: translateX(-4px) rotate(-12deg); }
}

Sass source 331 lines

@use 'sass:math';

// A retro rangefinder: a box body, a lens barrel of 16 strips around the Z axis, a glass disc
// whose reflection slides as the camera rocks, a flash unit and a shutter button on top.
$W: 144px;
$H: 88px;
$D: 40px;
$lx: 72px; // lens centre on the front face
$ly: 52px;
$R: 25px; // barrel radius
$L: 26px; // barrel length
$n: 16;
$sw: 2 * $R * math.tan(math.div(180deg, $n)); // strip width = side of the 16-gon
$rock: 7s;

.d-camera {
  --silver: #d9dce8;
  --silver-dark: #8a90a8;
  --leather: color-mix(in srgb, var(--accent) 16%, #14151d);
  position: relative;
  width: $W;
  height: $H;
  transform-style: preserve-3d;
  animation: d-camera-rock $rock ease-in-out infinite alternate;

  &__shadow {
    position: absolute;
    left: -16px;
    top: $H - 40px;
    width: $W + 32px;
    height: 80px;
    border-radius: 50%;
    background: radial-gradient(closest-side, rgb(0 0 0 / 0.42), transparent);
    transform: rotateX(90deg);
  }

  // --- the body: a plain box -----------------------------------------------------------------
  &__face {
    position: absolute;
    top: 0;
    left: 0;
    width: $W;
    height: $H;
    // silver top plate, leatherette band, silver bottom plate
    background:
      linear-gradient(transparent 24px, rgb(0 0 0 / 0.35) 24px 25px, transparent 25px 79px, rgb(255 255 255 / 0.5) 79px 80px, transparent 80px),
      radial-gradient(circle, rgb(255 255 255 / 0.07) 0.8px, transparent 1.2px) 0 0 / 3px 3px,
      linear-gradient(var(--silver) 0 3px, #f4f5fa 8px, var(--silver) 18px, var(--silver-dark) 24px, var(--leather) 24px 80px, var(--silver) 80px, var(--silver-dark));

    &--front {
      transform: translateZ($D * 0.5);
    }

    &--back {
      transform: rotateY(180deg) translateZ($D * 0.5);
    }

    &--left,
    &--right {
      left: ($W - $D) * 0.5;
      width: $D;
    }

    &--left {
      transform: rotateY(-90deg) translateZ($W * 0.5);
    }

    &--right {
      transform: rotateY(90deg) translateZ($W * 0.5);
    }

    &--top,
    &--bottom {
      top: ($H - $D) * 0.5;
      height: $D;
      background:
        repeating-linear-gradient(90deg, rgb(255 255 255 / 0.18) 0 1px, transparent 1px 3px),
        linear-gradient(var(--silver-dark), var(--silver) 20%, #f4f5fa 60%, var(--silver));
    }

    &--top {
      transform: rotateX(90deg) translateZ($H * 0.5);
    }

    &--bottom {
      background: linear-gradient(var(--silver-dark), #5b6078);
      transform: rotateX(-90deg) translateZ($H * 0.5);
    }

    // side faces are in shade
    &--left::after,
    &--right::after {
      content: '';
      position: absolute;
      inset: 0;
      background: rgb(0 0 0 / 0.3);
    }

    &--left::after {
      background: rgb(0 0 0 / 0.12);
    }
  }

  // viewfinder and rangefinder windows, and the engraved name, printed on the front face
  &__window {
    position: absolute;
    top: 6px;
    left: 11px;
    width: 26px;
    height: 13px;
    border: 1.5px solid var(--silver-dark);
    border-radius: 2px;
    background:
      linear-gradient(120deg, transparent 30%, rgb(255 255 255 / 0.45) 40%, transparent 50%),
      linear-gradient(160deg, color-mix(in srgb, var(--accent-2) 45%, #0b1020), #05060c);

    & + & {
      left: auto;
      right: 11px;
      width: 14px;
    }
  }

  &__name {
    position: absolute;
    top: 8px;
    left: 0;
    width: 100%;
    color: #4a4f66;
    font-size: 7px;
    font-weight: 800;
    letter-spacing: 2.5px;
    text-align: center;
    text-indent: 2.5px;
    text-shadow: 0 1px 0 rgb(255 255 255 / 0.7); // engraved: a light edge under dark letters
  }

  // --- the lens --------------------------------------------------------------------------
  // A 0×0 anchor at the lens centre, on the front face.
  &__lens {
    position: absolute;
    left: $lx;
    top: $ly;
    transform-style: preserve-3d;
    transform: translateZ($D * 0.5);
  }

  // the mount ring flush on the body
  &__mount {
    position: absolute;
    left: -$R - 6px;
    top: -$R - 6px;
    width: 2 * $R + 12px;
    height: 2 * $R + 12px;
    border-radius: 50%;
    background: radial-gradient(circle, #0b0c12 0 76%, var(--silver-dark) 80%, #f4f5fa 88%, var(--silver) 94%, var(--silver-dark));
    transform: translateZ(0.5px);
  }

  // Barrel strips: like the arc of a headband, but a full circle and pushed forward. Turn to
  // the strip's angle, move out by the radius, fold flat along Z, slide forward half a length.
  &__strip {
    --a: calc(var(--i) * #{math.div(360deg, $n)});
    position: absolute;
    left: -0.5 * $sw - 0.3px;
    top: -0.5 * $L;
    width: $sw + 0.6px;
    height: $L;
    backface-visibility: hidden;
    // top of the strip = back (at the body), bottom = front
    background:
      linear-gradient(rgb(0 0 0 / calc(0.36 - 0.34 * cos(var(--a) + 35deg))) 0 0),
      linear-gradient(transparent 0 5px, rgb(0 0 0 / 0.5) 5px 6px, transparent 6px 17px, var(--hot) 17px 18px, transparent 18px),
      repeating-linear-gradient(90deg, #2a2c3a 0 1.5px, #0d0e14 1.5px 3px) 0 6px / 100% 11px no-repeat,
      linear-gradient(#1a1b24 0 6px, #1a1b24 17px, var(--silver) 17px, #f4f5fa 22px, var(--silver-dark));
    transform: rotateZ(var(--a)) translateY(-$R) rotateX(90deg) translateY(0.5 * $L);
  }

  // the front ring and, a little further back, the glass
  &__ring,
  &__glass {
    position: absolute;
    border-radius: 50%;
  }

  &__ring {
    left: -$R - 0.5px;
    top: -$R - 0.5px;
    width: 2 * $R + 1px;
    height: 2 * $R + 1px;
    background: radial-gradient(circle, transparent 0 17px, #05060a 17.5px 18.5px, var(--silver-dark) 19px, #f4f5fa 21.5px, var(--silver) 23px, var(--silver-dark));
    transform: translateZ($L);
  }

  &__glass {
    left: -18px;
    top: -18px;
    width: 36px;
    height: 36px;
    overflow: hidden; // a flat disc, so clipping is safe
    background:
      radial-gradient(circle, #05060a 0 4px, transparent 5px),
      radial-gradient(circle, transparent 0 9px, color-mix(in srgb, var(--accent) 50%, transparent) 10px, transparent 12px),
      conic-gradient(from 200deg, color-mix(in srgb, var(--accent-2) 35%, #0a0c18), #0a0c18 25%, color-mix(in srgb, var(--accent) 45%, #0a0c18) 50%, #0a0c18 75%, color-mix(in srgb, var(--accent-2) 35%, #0a0c18));
    transform: translateZ($L - 4px);

    // The reflection of a window: slides across the glass the other way to the rocking.
    em {
      position: absolute;
      top: 5px;
      left: 9px;
      width: 12px;
      height: 16px;
      border-radius: 50% 50% 40% 40%;
      background:
        linear-gradient(90deg, transparent 45%, rgb(0 0 0 / 0.5) 45% 55%, transparent 55%),
        linear-gradient(transparent 45%, rgb(0 0 0 / 0.5) 45% 55%, transparent 55%),
        rgb(255 255 255 / 0.55);
      transform: translateX(6px) rotate(-12deg);
      animation: d-camera-glare $rock ease-in-out infinite alternate;
    }
  }

  // --- on top: flash unit and shutter ----------------------------------------------------
  &__flash {
    position: absolute;
    top: -20px;
    left: 12px;
    width: 40px;
    height: 20px;
    transform-style: preserve-3d;

    > i {
      position: absolute;
      inset: 0;
      background: linear-gradient(var(--silver), var(--silver-dark));
    }

    // Fresnel reflector
    > i:nth-child(1) {
      border: 2px solid var(--silver-dark);
      background:
        repeating-linear-gradient(transparent 0 1.5px, rgb(0 0 0 / 0.08) 1.5px 2.5px),
        radial-gradient(ellipse at 50% 40%, #fff, #e7e9f3 55%, #b9bed0);
      transform: translateZ(12px);
    }

    > i:nth-child(2) {
      transform: rotateY(180deg) translateZ(12px);
    }

    > i:nth-child(3),
    > i:nth-child(4) {
      left: 8px;
      width: 24px;
      background: linear-gradient(var(--silver-dark), #5b6078);
    }

    > i:nth-child(3) {
      transform: rotateY(90deg) translateZ(20px);
    }

    > i:nth-child(4) {
      transform: rotateY(-90deg) translateZ(20px);
    }

    > i:nth-child(5) {
      top: -2px;
      height: 24px;
      background: linear-gradient(90deg, var(--silver), #f4f5fa 50%, var(--silver));
      transform: rotateX(90deg);
    }
  }

  // Shutter button and dial: discs laid flat on the top, stacked for height.
  &__knob {
    position: absolute;
    left: 110px;
    top: -8px;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: radial-gradient(circle, var(--silver) 0 45%, var(--silver-dark));
    transform: translateY(calc(var(--h) * -1px)) rotateX(90deg);

    &--top {
      background: radial-gradient(circle at 40% 35%, #fff 0 8%, var(--hot) 35%, color-mix(in srgb, var(--hot) 55%, #000));
    }
  }

  &__dial {
    position: absolute;
    left: 74px;
    top: -12px;
    width: 24px;
    height: 24px;
    border-radius: 50%;
    background:
      repeating-conic-gradient(#5b6078 0 5deg, var(--silver) 5deg 10deg),
      var(--silver);
    transform: translateY(calc(var(--h) * -1px)) rotateX(90deg);

    &--top {
      background:
        linear-gradient(transparent 46%, var(--hot) 46% 54%, transparent 54%) 50% 0 / 2px 50% no-repeat,
        radial-gradient(circle, #f4f5fa, var(--silver) 60%, var(--silver-dark) 64%, transparent 66%),
        repeating-conic-gradient(#5b6078 0 5deg, var(--silver) 5deg 10deg);
    }
  }
}

@keyframes d-camera-rock {
  from {
    transform: rotateX(-14deg) rotateY(-30deg);
  }

  to {
    transform: rotateX(-8deg) rotateY(26deg);
  }
}

// the reflection moves against the turn: the window it mirrors stays where it is
@keyframes d-camera-glare {
  from {
    transform: translateX(10px) rotate(-12deg);
  }

  to {
    transform: translateX(-4px) rotate(-12deg);
  }
}
Standalone snippet · plain CSS, no build step, no dependencies.