Pure CSS Products & branding #loop #product #cone #packaging #label

Takeaway coffee in pure CSS

A paper cup that is wider at the top: twenty trapezoid strips lean out from the base circle. The sleeve and its logo wrap around without a seam, the lid is a stack of discs and the steam keeps rising.

  • 172 lines of CSS
  • 31 lines of HTML
  • No JavaScript
  • No dependencies
  • Snippet under MIT
GitHub

How it works

  1. A paper cup is a frustum: a cone with its tip cut off. Twenty strips stand on the base circle (rotateY(i × 18deg) translateZ(27 units)), hinged on their bottom edge, and lean out by atan((36 − 27) / 100) ≈ 5.14° so the rim is wider than the base.
  2. Each strip is a trapezoid cut with clip-path: 11.4 units wide at the rim, 8.6 units at the base (both are 2 · r · tan(9°)), each drawn 1.2 units wider so neighbours overlap and no seam shows. Its height is the slant length, √(100² + 9²).
  3. The sleeve and its logo are painted on one canvas, exactly one lap long, and every strip slides it by its index (background-position). The lap is measured at the sleeve's middle height, where the strips are 9.86 units wide, so the logo wraps round without a seam.
  4. The light stays put while the cup turns: every strip has a dark overlay whose opacity runs one lap, started i/20 of the way round with a negative animation-delay.
  5. The lid is five discs stacked 1.5 units apart plus a raised spout. The steam rises from the sip slot: it sits there inside the spinning part, so it goes round with the cup, and a counter-turn keeps each wisp facing you. Each S-shaped wisp rises, grows and fades, and starts and ends invisible so the loop never shows.
  6. Every length is a multiple of one base unit, --u, tied to the canvas, so the cup is the same share of a gallery card, the editor and a recording canvas.

Key techniques

  • frustum: strips hinged at the base, rotateX(−lean)
  • trapezoids with clip-path
  • one lap-long label, background-position by index
  • steam from the sip slot, counter-turned to face you

Copy-paste code

HTML 31 lines

<div class="scene">
  <div class="coffee">
    <div class="spin">
      <i class="shadow"></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="strip" style="--i:16"></i>
      <i class="strip" style="--i:17"></i>
      <i class="strip" style="--i:18"></i>
      <i class="strip" style="--i:19"></i>
      <i class="lid" style="--y:-1.5"></i><i class="lid" style="--y:0"></i><i class="lid" style="--y:1.5"></i><i class="lid" style="--y:3"></i>
      <i class="lid lid-top" style="--y:4.5"></i>
      <i class="lid spout" style="--y:6.5"></i>
      <i class="vent"><i class="face"><i class="steam" style="--x:-2;--d:0"></i><i class="steam" style="--x:3;--d:1"></i></i></i>
    </div>
  </div>
</div>

CSS 172 lines

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

/* a 0-wide column, 100 units tall: the cup's axis */
.coffee {
  --paper: #f4f1ea;
  --paper-dark: #d9d3c6;
  --kraft: #c1834a;
  --violet: #8b6cff;
  --lid: #22232e;
  position: relative;
  width: 0;
  height: calc(100 * var(--u));
  transform-style: preserve-3d;
  /* the lid rises a little above this column: move it down by that much, and the solid cup is
     centred. The steam is faint and rises well above it, so it is left out of that sum: the eye
     places the cup by its body */
  translate: 0 calc(3 * var(--u));
  transform: rotateX(-20deg);
}

.spin {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  animation: spin 12s linear infinite;
}

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

/* 20 trapezoids hinged on the base circle (r 27 units), leaning out towards the rim (r 36 units) */
.strip {
  position: absolute;
  left: calc(-6.3 * var(--u));
  bottom: 0;
  width: calc(12.6 * var(--u));      /* 11.4 units at the rim, plus a little overlap */
  height: calc(100.4 * var(--u));    /* the slant length */
  transform-origin: 50% 100%;
  transform: rotateY(calc(var(--i) * 18deg)) translateZ(calc(27 * var(--u))) rotateX(-5.14deg);
  clip-path: polygon(0 0, 100% 0, calc(50% + 4.9 * var(--u)) 100%, calc(50% - 4.9 * var(--u)) 100%);
  backface-visibility: hidden;
  /* a soft shade under the lid: the lid's discs are nearly side-on, and their edge against
     bare white paper steps and reads as a dashed line; against the shade it reads as one edge */
  box-shadow: inset 0 calc(5 * var(--u)) calc(3 * var(--u)) calc(-2 * var(--u)) rgb(20 20 30 / 0.8);
  /* one 197 units canvas = one lap round the sleeve's middle; each strip shows its own slice */
  background-image:
    radial-gradient(ellipse calc(1.2 * var(--u)) calc(5 * var(--u)) at calc(49.3 * var(--u)) calc(53 * var(--u)), #f0d9b8 0 60%, transparent 90%),
    radial-gradient(ellipse calc(4.5 * var(--u)) calc(6.5 * var(--u)) at calc(49.3 * var(--u)) calc(53 * var(--u)), #fff 0 20%, #6b3e22 30% 92%, transparent),
    radial-gradient(circle calc(11 * var(--u)) at calc(49.3 * var(--u)) calc(53 * var(--u)), var(--violet) 0 86%, #fff 87% 93%, var(--violet) 94% 100%, transparent 100%),
    radial-gradient(ellipse calc(1.2 * var(--u)) calc(5 * var(--u)) at calc(148 * var(--u)) calc(53 * var(--u)), #f0d9b8 0 60%, transparent 90%),
    radial-gradient(ellipse calc(4.5 * var(--u)) calc(6.5 * var(--u)) at calc(148 * var(--u)) calc(53 * var(--u)), #fff 0 20%, #6b3e22 30% 92%, transparent),
    radial-gradient(circle calc(11 * var(--u)) at calc(148 * var(--u)) calc(53 * var(--u)), var(--violet) 0 86%, #fff 87% 93%, var(--violet) 94% 100%, transparent 100%),
    linear-gradient(transparent calc(34 * var(--u)), var(--kraft) calc(34 * var(--u)) calc(36 * var(--u)), #d8b08c calc(36 * var(--u)) calc(37 * var(--u)), var(--kraft) calc(37 * var(--u)) calc(70 * var(--u)), #d8b08c calc(70 * var(--u)) calc(71 * var(--u)), var(--kraft) calc(71 * var(--u)) calc(73 * var(--u)), transparent calc(73 * var(--u))),
    linear-gradient(var(--paper-dark) 0 calc(1 * var(--u)), var(--paper) calc(3 * var(--u)) calc(12 * var(--u)), var(--violet) calc(12 * var(--u)) calc(14 * var(--u)), var(--paper) calc(14 * var(--u)) calc(100% - 3 * var(--u)), var(--paper-dark));
  background-size: calc(197.3 * var(--u)) 100%;
  background-position: calc(1.07 * var(--u) - var(--i) * 9.864 * var(--u)) 0;
}

/* lighting that stays put: one lap of shade, each strip started i/20 of the way round */
.strip::after {
  content: '';
  position: absolute;
  inset: 0;
  background: #06070f;
  animation: shade 12s linear infinite;
  animation-delay: calc(var(--i) * -0.6s);
}

/* discs centred on the rim, lifted by --y, laid flat */
.lid {
  --r: calc(38.4 * var(--u));
  position: absolute;
  left: calc(var(--r) * -1);
  top: calc(var(--r) * -1);
  width: calc(var(--r) * 2);
  height: calc(var(--r) * 2);
  border-radius: 50%;
  background: radial-gradient(circle, var(--lid) 0 80%, #646570 94%, var(--lid));
  transform: translateY(calc(var(--y) * -1 * var(--u))) rotateX(90deg);
}

.lid-top {
  --r: calc(35.4 * var(--u));
  background:
    radial-gradient(circle, transparent 0 70%, rgb(255 255 255 / 0.12) 72% 74%, transparent 76%),
    radial-gradient(circle at 35% 30%, #4e4f58, var(--lid) 60%);
}

.spout {
  --r: calc(13 * var(--u));
  background:
    radial-gradient(ellipse calc(7 * var(--u)) calc(2.5 * var(--u)) at 50% 70%, #05060a 0 90%, transparent), /* the sip slot */
    radial-gradient(circle at 40% 30%, #6a6b74, var(--lid) 70%);
  transform: translateY(calc(var(--y) * -1 * var(--u))) translateZ(calc(19 * var(--u))) rotateX(90deg);
}

/* steam rises from the sip slot: placed there inside .spin, so it goes round with the cup... */
.vent {
  position: absolute;
  top: calc(-7 * var(--u));
  left: 0;
  transform-style: preserve-3d;
  transform: translateZ(calc(24 * var(--u)));
}

/* ...and turned back the other way, so the wisps always face you */
.face {
  position: absolute;
  transform-style: preserve-3d;
  animation: spin 12s linear infinite reverse;
}

/* two arcs make an S */
.steam {
  position: absolute;
  top: calc(-30 * var(--u));
  left: calc(-5 * var(--u));
  width: calc(10 * var(--u));
  height: calc(30 * var(--u));
  opacity: 0;
  animation: steam 3.6s linear infinite;
  animation-delay: calc(var(--d) * -1.8s);
}

.steam::before,
.steam::after {
  content: '';
  position: absolute;
  left: 0;
  box-sizing: border-box;
  width: 100%;
  height: 52%;
  border: calc(2.5 * var(--u)) solid transparent;
  border-radius: 50%;
}

.steam::before { top: 0; border-left-color: rgb(236 238 251 / 0.5); }
.steam::after { bottom: 0; left: -70%; border-right-color: rgb(236 238 251 / 0.5); }

@keyframes spin {
  to { transform: rotateY(360deg); }
}

/* 0% = facing you; between 25% and 75% the strip faces away */
@keyframes shade {
  0%       { opacity: 0.06; }
  12.5%    { opacity: 0.3; }
  25%, 50% { opacity: 0.5; }
  75%      { opacity: 0.3; }
  88%      { opacity: 0; }
  100%     { opacity: 0.06; }
}

@keyframes steam {
  0%   { opacity: 0; transform: translate(calc(var(--x) * 1 * var(--u)), calc(10 * var(--u))) scale(0.6); }
  30%  { opacity: 0.9; }
  60%  { transform: translate(calc(var(--x) * 1 * var(--u) + 4 * var(--u)), calc(-12 * var(--u))) scale(0.9, 1.1); }
  100% { opacity: 0; transform: translate(calc(var(--x) * 1 * var(--u) - 2 * var(--u)), calc(-34 * var(--u))) scale(1.1, 1.3); }
}
Standalone snippet · plain CSS, no build step, no dependencies.