Pure CSS Shapes & solids #loop #gear #machine #cog

Spinning gear in pure CSS

A thick eight-tooth gear driving a smaller five-tooth one the other way. Each face is one clip-path polygon, and every straight edge of it gets a standing rectangle, so the gear has a real rim and real teeth.

  • 120 lines of CSS
  • 25 lines of HTML
  • No JavaScript
  • No dependencies
  • Snippet under MIT
GitHub

How it works

  1. A gear face is one element cut to the outline with clip-path: polygon(): eight rectangular teeth on a circle, and a straight chord across each gap. Two copies, front and back, are pushed apart with translateZ(±8). That is the whole silhouette for four elements, where teeth built as boxes would cost dozens.
  2. A flat face seen edge-on draws nothing, so the thickness is real: a standing rectangle on every straight edge of the outline. The tip of tooth i is rotateZ(i × 45deg) translateY(-80) rotateX(90deg), its two flanks are translate(±11, -71) rotateY(90deg) in the same turned frame, and the rim chord in the gap after it sits at the half step. The chord is exactly as long and as far out as the clip-path edge it stands on, so the pieces meet with no seam.
  3. Two gears mesh when their teeth are the same size, so the small one has the same tooth width and height and a pitch radius in the ratio of the tooth counts: 72 × 5 / 8 = 45. Their centres are one pitch radius each apart, 117, and the small gear starts turned 45° so a gap of its faces the big tooth that points at it.
  4. The big gear turns 360° in 12 s; the small one turns -576deg, 8/5 of a turn the other way, in the same 12 s. 576° is exactly eight teeth of a five-tooth gear, so the end pose is the start pose and the linear loop has no join.
  5. The tilt is a static rotateX(55deg) on a wrapper and only the gears spin inside it, both with preserve-3d. Every length is a multiple of one base unit, --u, so the pair is the same share of a gallery card, the editor and a recording canvas; the numbers above are those units.

Key techniques

  • clip-path: polygon() face
  • standing rectangles on every edge
  • radii in the ratio of tooth counts
  • counter-spin at -8/5 of a turn

Copy-paste code

HTML 25 lines

<div class="scene">
  <div class="rig">
  <div class="gear big">
    <b class="face front"></b>
    <b class="face back"></b>
    <i class="tip" style="--i:0"></i><i class="fl l" style="--i:0"></i><i class="fl r" style="--i:0"></i><i class="rim" style="--i:0"></i>
    <i class="tip" style="--i:1"></i><i class="fl l" style="--i:1"></i><i class="fl r" style="--i:1"></i><i class="rim" style="--i:1"></i>
    <i class="tip" style="--i:2"></i><i class="fl l" style="--i:2"></i><i class="fl r" style="--i:2"></i><i class="rim" style="--i:2"></i>
    <i class="tip" style="--i:3"></i><i class="fl l" style="--i:3"></i><i class="fl r" style="--i:3"></i><i class="rim" style="--i:3"></i>
    <i class="tip" style="--i:4"></i><i class="fl l" style="--i:4"></i><i class="fl r" style="--i:4"></i><i class="rim" style="--i:4"></i>
    <i class="tip" style="--i:5"></i><i class="fl l" style="--i:5"></i><i class="fl r" style="--i:5"></i><i class="rim" style="--i:5"></i>
    <i class="tip" style="--i:6"></i><i class="fl l" style="--i:6"></i><i class="fl r" style="--i:6"></i><i class="rim" style="--i:6"></i>
    <i class="tip" style="--i:7"></i><i class="fl l" style="--i:7"></i><i class="fl r" style="--i:7"></i><i class="rim" style="--i:7"></i>
  </div>
  <div class="gear small">
    <b class="face front"></b>
    <b class="face back"></b>
    <i class="tip" style="--i:0"></i><i class="fl l" style="--i:0"></i><i class="fl r" style="--i:0"></i><i class="rim" style="--i:0"></i>
    <i class="tip" style="--i:1"></i><i class="fl l" style="--i:1"></i><i class="fl r" style="--i:1"></i><i class="rim" style="--i:1"></i>
    <i class="tip" style="--i:2"></i><i class="fl l" style="--i:2"></i><i class="fl r" style="--i:2"></i><i class="rim" style="--i:2"></i>
    <i class="tip" style="--i:3"></i><i class="fl l" style="--i:3"></i><i class="fl r" style="--i:3"></i><i class="rim" style="--i:3"></i>
    <i class="tip" style="--i:4"></i><i class="fl l" style="--i:4"></i><i class="fl r" style="--i:4"></i><i class="rim" style="--i:4"></i>
  </div>
  </div>
</div>

CSS 120 lines

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

/* the static tilt; the gears spin inside it. The inner translate, in the gears' own plane,
   centres the pair (the small gear hangs off the big one's top right); the outer one, in screen
   space, is the last few units the perspective pulls it off by (the near, bottom side is drawn
   larger than the far one) */
.rig {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  transform: translate(calc(5 * var(--u)), calc(-8 * var(--u))) rotateX(55deg) translate(calc(-28 * var(--u)), calc(28 * var(--u)));
}

.gear {
  position: absolute;
  top: 50%;
  left: 50%;
  transform-style: preserve-3d;
  animation: 12s linear infinite;
}

/* 8 teeth, tip radius 80, root 62: a 160 box. The tooth step is 45deg */
.big {
  --step: 45deg;
  --rt: 80;
  --rm: 71;           /* half way between root and tip: where a flank's middle sits */
  --rw: 27.13;        /* the rim chord in a gap: its length ... */
  --rd: 61.49;        /* ... and how far out it stands */
  width: calc(160 * var(--u));
  height: calc(160 * var(--u));
  margin: calc(-80 * var(--u)) 0 0 calc(-80 * var(--u));
  animation-name: gear-cw;
}

/* 5 teeth, tip 53, root 35: a 106 box, its centre 117 up-right of the big one's at 45deg
   (117 × sin 45 = 82.7). Pitch radii 72 + 45 = 117 is what makes the teeth mesh */
.small {
  --step: 72deg;
  --rt: 53;
  --rm: 44;
  --rw: 23.35;
  --rd: 34.78;
  width: calc(106 * var(--u));
  height: calc(106 * var(--u));
  margin: calc(-135.7 * var(--u)) 0 0 calc(29.7 * var(--u));
  animation-name: gear-ccw;
}

/* the two faces: one polygon each, 8 apart in depth */
.face {
  position: absolute;
  inset: 0;
}
.big .face { clip-path: polygon(43.13% 11.25%, 43.13% 0%, 56.88% 0%, 56.88% 11.25%, 72.54% 17.74%, 80.49% 9.78%, 90.22% 19.51%, 82.26% 27.46%, 88.75% 43.13%, 100% 43.13%, 100% 56.88%, 88.75% 56.88%, 82.26% 72.54%, 90.22% 80.49%, 80.49% 90.22%, 72.54% 82.26%, 56.88% 88.75%, 56.88% 100%, 43.13% 100%, 43.13% 88.75%, 27.46% 82.26%, 19.51% 90.22%, 9.78% 80.49%, 17.74% 72.54%, 11.25% 56.88%, 0% 56.88%, 0% 43.13%, 11.25% 43.13%, 17.74% 27.46%, 9.78% 19.51%, 19.51% 9.78%, 27.46% 17.74%); }
.small .face { clip-path: polygon(39.62% 16.98%, 39.62% 0%, 60.38% 0%, 60.38% 16.98%, 78.2% 29.93%, 94.35% 24.68%, 100.76% 44.42%, 84.61% 49.67%, 77.8% 70.61%, 87.78% 84.35%, 70.99% 96.55%, 61.01% 82.81%, 38.99% 82.81%, 29.01% 96.55%, 12.22% 84.35%, 22.2% 70.61%, 15.39% 49.67%, -0.76% 44.42%, 5.65% 24.68%, 21.8% 29.93%); }
.front { transform: translateZ(calc(8 * var(--u))); }
.back { transform: translateZ(calc(-8 * var(--u))); }

/* the top face carries a painted hub ring, so the eye has a centre to see the turn against */
.big .front { background: radial-gradient(circle, #c9bcff 0 7%, #4a2fb8 7.5% 12%, #a390ff 12.5%, #8b6cff 55%, #7657f0); }
.big .back, .big .rim, .big .tip { background: #5f42d6; }
.big .fl { background: #4e33be; }
.small .front { background: radial-gradient(circle, #d6fffb 0 9%, #12857a 9.5% 15%, #7cf2e8 15.5%, #2ee6d6 55%, #22ccbd); }
.small .back, .small .rim, .small .tip { background: #159c90; }
.small .fl { background: #117f75; }

/* every standing piece starts centred on the gear, then is turned to its tooth, moved out and
   stood up. rotateX(90deg) stands a piece on a tangent (its height becomes depth, centred on the
   faces); rotateY(90deg) stands one on a radius */
.gear i {
  position: absolute;
  top: 50%;
  left: 50%;
}

/* a tooth's tip: 22 wide, the thickness tall, at the tip radius */
.tip {
  width: calc(22 * var(--u));
  height: calc(16 * var(--u));
  margin: calc(-8 * var(--u)) 0 0 calc(-11 * var(--u));
  transform: rotateZ(calc(var(--i) * var(--step))) translateY(calc(-1 * var(--rt) * var(--u))) rotateX(90deg);
}

/* a flank: the thickness wide, the tooth height (18) tall, 11 either side of the tooth's centre
   line, its middle half way out the tooth */
.fl {
  width: calc(16 * var(--u));
  height: calc(18 * var(--u));
  margin: calc(-9 * var(--u)) 0 0 calc(-8 * var(--u));
}
.fl.l { transform: rotateZ(calc(var(--i) * var(--step))) translate(calc(-11 * var(--u)), calc(-1 * var(--rm) * var(--u))) rotateY(90deg); }
.fl.r { transform: rotateZ(calc(var(--i) * var(--step))) translate(calc(11 * var(--u)), calc(-1 * var(--rm) * var(--u))) rotateY(90deg); }

/* the rim in the gap after tooth i: a chord at the half step, sized to meet both flanks' feet */
.rim {
  width: calc(var(--rw) * var(--u));
  height: calc(16 * var(--u));
  margin: calc(-8 * var(--u)) 0 0 calc(-0.5 * var(--rw) * var(--u));
  transform: rotateZ(calc((var(--i) + 0.5) * var(--step))) translateY(calc(-1 * var(--rd) * var(--u))) rotateX(90deg);
}

@keyframes gear-cw {
  from { transform: rotateZ(0deg); }
  to   { transform: rotateZ(360deg); }
}

/* starts at 45deg so a gap faces the big gear's tooth; -576deg is 8/5 of a turn, eight of its
   five teeth, so it ends on the pose it started in */
@keyframes gear-ccw {
  from { transform: rotateZ(45deg); }
  to   { transform: rotateZ(-531deg); }
}
Standalone snippet · plain CSS, no build step, no dependencies.