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.

  • 252 lines of CSS
  • 35 lines of HTML
  • No JavaScript
  • No dependencies
  • Snippet under MIT
GitHub

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) and stacked (the button's four 1.5 units apart, the dial's two 3 units apart): from above they read as short cylinders.
  6. Every length is a multiple of one base unit, --u, tied to the canvas, so the camera is the same share of a gallery card, the editor and a recording canvas.

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 252 lines

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

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

.shadow {
  position: absolute;
  left: calc(-16 * var(--u));
  top: calc(48 * var(--u));
  width: calc(176 * var(--u));
  height: calc(80 * var(--u));
  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: calc(144 * var(--u));
  height: calc(88 * var(--u));
  /* silver plate, a seam, leatherette with grain, a seam, silver plate */
  background:
    linear-gradient(transparent calc(24 * var(--u)), rgb(0 0 0 / 0.35) calc(24 * var(--u)) calc(25 * var(--u)), transparent calc(25 * var(--u)) calc(79 * var(--u)), rgb(255 255 255 / 0.5) calc(79 * var(--u)) calc(80 * var(--u)), transparent calc(80 * var(--u))),
    radial-gradient(circle, rgb(255 255 255 / 0.07) calc(0.8 * var(--u)), transparent calc(1.2 * var(--u))) 0 0 / calc(3 * var(--u)) calc(3 * var(--u)),
    linear-gradient(var(--silver) 0 calc(3 * var(--u)), #f4f5fa calc(8 * var(--u)), var(--silver) calc(18 * var(--u)), var(--silver-dark) calc(24 * var(--u)), var(--leather) calc(24 * var(--u)) calc(80 * var(--u)), var(--silver) calc(80 * var(--u)), var(--silver-dark));
}

.front { transform: translateZ(calc(20 * var(--u))); }
.back  { transform: rotateY(180deg) translateZ(calc(20 * var(--u))); }
.left, .right { left: calc(52 * var(--u)); width: calc(40 * var(--u)); }
.left  { transform: rotateY(-90deg) translateZ(calc(72 * var(--u))); }
.right { transform: rotateY(90deg) translateZ(calc(72 * var(--u))); }
.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: calc(24 * var(--u)); height: calc(40 * var(--u)); }
.top {
  background:
    repeating-linear-gradient(90deg, rgb(255 255 255 / 0.18) 0 calc(1 * var(--u)), transparent calc(1 * var(--u)) calc(3 * var(--u))),
    linear-gradient(var(--silver-dark), var(--silver) 20%, #f4f5fa 60%, var(--silver));
  transform: rotateX(90deg) translateZ(calc(44 * var(--u)));
}
.bottom {
  background: linear-gradient(var(--silver-dark), #5b6078);
  transform: rotateX(-90deg) translateZ(calc(44 * var(--u)));
}

.window {
  position: absolute;
  top: calc(6 * var(--u));
  left: calc(11 * var(--u));
  box-sizing: border-box;
  width: calc(26 * var(--u));
  height: calc(13 * var(--u));
  border: calc(1.5 * var(--u)) solid var(--silver-dark);
  border-radius: calc(2 * var(--u));
  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: calc(11 * var(--u)); width: calc(14 * var(--u)); }

.name {
  position: absolute;
  top: calc(8 * var(--u));
  width: 100%;
  color: #4a4f66;
  font: 800 calc(7 * var(--u)) Inter, system-ui, sans-serif;
  letter-spacing: calc(2.5 * var(--u));
  text-align: center;
  text-indent: calc(2.5 * var(--u));
  text-shadow: 0 calc(1 * var(--u)) 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: calc(72 * var(--u));
  top: calc(52 * var(--u));
  transform-style: preserve-3d;
  transform: translateZ(calc(20 * var(--u)));
}

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

/* 16 strips, radius 25 units, 26 units long. Strip top = at the body, bottom = the front. */
.strip {
  --a: calc(var(--i) * 22.5deg);
  position: absolute;
  left: calc(-5.3 * var(--u));
  top: calc(-13 * var(--u));
  width: calc(10.6 * var(--u));
  height: calc(26 * var(--u));
  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 calc(5 * var(--u)), rgb(0 0 0 / 0.5) calc(5 * var(--u)) calc(6 * var(--u)), transparent calc(6 * var(--u)) calc(17 * var(--u)), #ff4d9d calc(17 * var(--u)) calc(18 * var(--u)), transparent calc(18 * var(--u))),
    repeating-linear-gradient(90deg, #2a2c3a 0 calc(1.5 * var(--u)), #0d0e14 calc(1.5 * var(--u)) calc(3 * var(--u))) 0 calc(6 * var(--u)) / 100% calc(11 * var(--u)) no-repeat, /* knurled grip */
    linear-gradient(#1a1b24 0 calc(17 * var(--u)), var(--silver) calc(17 * var(--u)), #f4f5fa calc(22 * var(--u)), var(--silver-dark));
  transform: rotateZ(var(--a)) translateY(calc(-25 * var(--u))) rotateX(90deg) translateY(calc(13 * var(--u)));
}

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

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

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

/* the reflection of a window: runs the rocking in reverse */
.glass em {
  position: absolute;
  top: calc(5 * var(--u));
  left: calc(9 * var(--u));
  width: calc(12 * var(--u));
  height: calc(16 * var(--u));
  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: calc(-20 * var(--u));
  left: calc(12 * var(--u));
  width: calc(40 * var(--u));
  height: calc(20 * var(--u));
  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: calc(2 * var(--u)) solid var(--silver-dark);
  background:
    repeating-linear-gradient(transparent 0 calc(1.5 * var(--u)), rgb(0 0 0 / 0.08) calc(1.5 * var(--u)) calc(2.5 * var(--u))), /* Fresnel lines */
    radial-gradient(ellipse at 50% 40%, #fff, #e7e9f3 55%, #b9bed0);
  transform: translateZ(calc(12 * var(--u)));
}
.flash i:nth-child(2) { transform: rotateY(180deg) translateZ(calc(12 * var(--u))); }
.flash i:nth-child(3),
.flash i:nth-child(4) { left: calc(8 * var(--u)); width: calc(24 * var(--u)); background: linear-gradient(var(--silver-dark), #5b6078); }
.flash i:nth-child(3) { transform: rotateY(90deg) translateZ(calc(20 * var(--u))); }
.flash i:nth-child(4) { transform: rotateY(-90deg) translateZ(calc(20 * var(--u))); }
.flash i:nth-child(5) {
  top: calc(-2 * var(--u));
  height: calc(24 * var(--u));
  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) * -1 * var(--u))) rotateX(90deg);
}

.knob {
  left: calc(110 * var(--u));
  top: calc(-8 * var(--u));
  width: calc(16 * var(--u));
  height: calc(16 * var(--u));
  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: calc(74 * var(--u));
  top: calc(-12 * var(--u));
  width: calc(24 * var(--u));
  height: calc(24 * var(--u));
  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 / calc(2 * var(--u)) 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(calc(10 * var(--u))) rotate(-12deg); }
  to   { transform: translateX(calc(-4 * var(--u))) rotate(-12deg); }
}
Standalone snippet · plain CSS, no build step, no dependencies.