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.

  • 162 lines of CSS
  • 31 lines of HTML
  • No JavaScript
  • No dependencies
  • MIT licensed

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(27px)), 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.4px wide at the rim, 8.6px at the base (both are 2 · r · tan(9°)). 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.86px 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.5px 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.

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

.scene {
  perspective: 800px;
}

/* a 0-wide column, 100px tall: the cup's axis */
.coffee {
  --paper: #f4f1ea;
  --paper-dark: #d9d3c6;
  --kraft: #c1834a;
  --violet: #8b6cff;
  --lid: #22232e;
  position: relative;
  width: 0;
  height: 100px;
  transform-style: preserve-3d;
  transform: rotateX(-20deg);
}

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

.shadow {
  position: absolute;
  left: -60px;
  top: 70px;
  width: 120px;
  height: 60px;
  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 27px), leaning out towards the rim (r 36px) */
.strip {
  position: absolute;
  left: -6.3px;
  bottom: 0;
  width: 12.6px;      /* 11.4px at the rim, plus a little overlap */
  height: 100.4px;    /* the slant length */
  transform-origin: 50% 100%;
  transform: rotateY(calc(var(--i) * 18deg)) translateZ(27px) rotateX(-5.14deg);
  clip-path: polygon(0 0, 100% 0, calc(50% + 4.9px) 100%, calc(50% - 4.9px) 100%);
  backface-visibility: hidden;
  /* one 197px canvas = one lap round the sleeve's middle; each strip shows its own slice */
  background-image:
    radial-gradient(ellipse 1.2px 5px at 49.3px 53px, #f0d9b8 0 60%, transparent 90%),
    radial-gradient(ellipse 4.5px 6.5px at 49.3px 53px, #fff 0 20%, #6b3e22 30% 92%, transparent),
    radial-gradient(circle 11px at 49.3px 53px, var(--violet) 0 86%, #fff 87% 93%, var(--violet) 94% 100%, transparent 100%),
    radial-gradient(ellipse 1.2px 5px at 148px 53px, #f0d9b8 0 60%, transparent 90%),
    radial-gradient(ellipse 4.5px 6.5px at 148px 53px, #fff 0 20%, #6b3e22 30% 92%, transparent),
    radial-gradient(circle 11px at 148px 53px, var(--violet) 0 86%, #fff 87% 93%, var(--violet) 94% 100%, transparent 100%),
    linear-gradient(transparent 34px, var(--kraft) 34px 36px, #d8b08c 36px 37px, var(--kraft) 37px 70px, #d8b08c 70px 71px, var(--kraft) 71px 73px, transparent 73px),
    linear-gradient(var(--paper-dark) 0 1px, var(--paper) 3px 12px, var(--violet) 12px 14px, var(--paper) 14px calc(100% - 3px), var(--paper-dark));
  background-size: 197.3px 100%;
  background-position: calc(1.07px - var(--i) * 9.864px) 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: 38.4px;
  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) * -1px)) rotateX(90deg);
}

.lid-top {
  --r: 35.4px;
  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: 13px;
  background:
    radial-gradient(ellipse 7px 2.5px 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) * -1px)) translateZ(19px) rotateX(90deg);
}

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

/* ...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: -30px;
  left: -5px;
  width: 10px;
  height: 30px;
  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: 2.5px 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) * 1px), 10px) scale(0.6); }
  30%  { opacity: 0.9; }
  60%  { transform: translate(calc(var(--x) * 1px + 4px), -12px) scale(0.9, 1.1); }
  100% { opacity: 0; transform: translate(calc(var(--x) * 1px - 2px), -34px) scale(1.1, 1.3); }
}

Sass source 227 lines

@use 'sass:math';

// A takeaway cup: a frustum (a cone with its tip cut off) of 20 leaning trapezoid strips, a kraft
// sleeve whose logo wraps around it, a lid of stacked discs, and steam that rises from the sip slot.
$n: 20;
$rt: 36px; // radius at the rim (to the middle of a strip's top edge)
$rb: 27px; // radius at the base
$h: 100px; // height
$turn: 12s;
$slant: math.hypot($h, $rt - $rb); // a strip's real length
$lean: math.atan(math.div($rt - $rb, $h)); // leaning OUT, so the top is wider
$half: math.tan(math.div(180deg, $n));
$wt: 2 * $rt * $half; // strip width at the rim
$wb: 2 * $rb * $half; // ... and at the base
// The sleeve art is laid out for the sleeve's middle height: there a strip is exactly $wm wide,
// so a strip's slice of the art lines up with its neighbours' (the logo has no seams).
$wm: 2 * ($rt - ($rt - $rb) * 0.54) * $half;
$lap: $wm * $n;
$rim: math.div($rt, math.cos(math.div(180deg, $n))) + 2px; // lid radius, just past the corners

.d-coffeecup {
  --paper: #f4f1ea;
  --paper-dark: #d9d3c6;
  --kraft: color-mix(in srgb, var(--warm) 45%, #8a5a33);
  --lid: #22232e;
  --steam: color-mix(in srgb, var(--text) 50%, transparent);
  position: relative;
  width: 0;
  height: $h;
  transform-style: preserve-3d;
  transform: rotateX(-20deg);

  &__spin {
    position: absolute;
    inset: 0;
    transform-style: preserve-3d;
    animation: d-coffeecup-spin $turn linear infinite;
  }

  &__shadow {
    position: absolute;
    left: -60px;
    top: $h - 30px;
    width: 120px;
    height: 60px;
    border-radius: 50%;
    background: radial-gradient(closest-side, rgb(0 0 0 / 0.45), transparent);
    transform: rotateX(90deg);
  }

  // --- the body ------------------------------------------------------------------------------
  // Each strip stands on the base circle, hinged on its bottom edge, and leans out by
  // atan((rt - rb) / h). It is a trapezoid: $wt wide at the top, $wb at the bottom.
  &__strip {
    position: absolute;
    left: -0.5 * $wt - 0.6px;
    bottom: 0;
    width: $wt + 1.2px; // overlap the neighbours a little: no seams
    height: $slant;
    transform-origin: 50% 100%;
    transform: rotateY(calc(var(--i) * #{math.div(360deg, $n)})) translateZ($rb) rotateX(-$lean);
    clip-path: polygon(0 0, 100% 0, calc(50% + #{$wb * 0.5 + 0.6px}) 100%, calc(50% - #{$wb * 0.5 + 0.6px}) 100%);
    backface-visibility: hidden;
    // All layers are painted on ONE lap-long canvas ($lap × 100%) and every strip slides it by
    // its index, so strip i shows slice i: the canvas wraps around the cup.
    background-image:
      // logo: a violet seal with a white coffee bean, twice round the cup
      radial-gradient(ellipse 1.2px 5px at #{$lap * 0.25} 53px, #f0d9b8 0 60%, transparent 90%),
      radial-gradient(ellipse 4.5px 6.5px at #{$lap * 0.25} 53px, #fff 0 20%, #6b3e22 30% 92%, transparent 100%),
      radial-gradient(circle 11px at #{$lap * 0.25} 53px, var(--accent) 0 86%, #fff 87% 93%, var(--accent) 94% 100%, transparent 100%),
      radial-gradient(ellipse 1.2px 5px at #{$lap * 0.75} 53px, #f0d9b8 0 60%, transparent 90%),
      radial-gradient(ellipse 4.5px 6.5px at #{$lap * 0.75} 53px, #fff 0 20%, #6b3e22 30% 92%, transparent 100%),
      radial-gradient(circle 11px at #{$lap * 0.75} 53px, var(--accent) 0 86%, #fff 87% 93%, var(--accent) 94% 100%, transparent 100%),
      // the sleeve: kraft band with a pinstripe near each edge
      linear-gradient(transparent 34px, var(--kraft) 34px 36px, color-mix(in srgb, var(--kraft) 70%, #fff) 36px 37px, var(--kraft) 37px 70px, color-mix(in srgb, var(--kraft) 70%, #fff) 70px 71px, var(--kraft) 71px 73px, transparent 73px),
      // paper, with a violet stripe under the rim
      linear-gradient(var(--paper-dark) 0 1px, var(--paper) 3px 12px, var(--accent) 12px 14px, var(--paper) 14px calc(100% - 3px), var(--paper-dark));
    background-size: $lap 100%;
    background-position: calc(#{($wt - $wm) * 0.5 + 0.3px} - var(--i) * #{$wm}) 0;

    // lighting that stays put while the cup turns (same idea as the drinks can)
    &::after {
      content: '';
      position: absolute;
      inset: 0;
      background: #06070f;
      animation: d-coffeecup-shade $turn linear infinite;
      animation-delay: calc(var(--i) * #{math.div($turn, -$n)});
    }
  }

  // --- the lid: discs laid flat, stacked for thickness -------------------------------------------
  &__lid {
    --r: #{$rim};
    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%, color-mix(in srgb, var(--lid) 70%, #fff) 94%, var(--lid));
    transform: translateY(calc(var(--y) * -1px)) rotateX(90deg);

    // the recessed top, a little smaller
    &--top {
      --r: #{$rim - 3px};
      background:
        radial-gradient(circle, transparent 0 70%, rgb(255 255 255 / 0.12) 72% 74%, transparent 76%),
        radial-gradient(circle at 35% 30%, color-mix(in srgb, var(--lid) 80%, #fff), var(--lid) 60%);
    }

    // the raised sip spout, toward the front, with its slot
    &--spout {
      --r: 13px;
      background:
        radial-gradient(ellipse 7px 2.5px at 50% 70%, #05060a 0 90%, transparent 100%),
        radial-gradient(circle at 40% 30%, color-mix(in srgb, var(--lid) 65%, #fff), var(--lid) 70%);
      transform: translateY(calc(var(--y) * -1px)) translateZ(19px) rotateX(90deg);
    }
  }

  // --- steam: rises from the sip slot, so it turns round with the cup; a counter-turn on the
  // inside keeps each wisp facing you (a flat wisp seen edge-on would vanish) -------------------------
  &__vent {
    position: absolute;
    top: -7px; // the top of the spout
    left: 0;
    transform-style: preserve-3d;
    transform: translateZ(24px); // over the slot, near the spout's front
  }

  &__face {
    position: absolute;
    transform-style: preserve-3d;
    animation: d-coffeecup-spin $turn linear infinite reverse;
  }

  &__steam {
    position: absolute;
    top: -30px;
    left: -5px;
    width: 10px;
    height: 30px;
    opacity: 0;
    animation: d-coffeecup-steam 3.6s linear infinite;
    animation-delay: calc(var(--d) * -1.8s);

    // two arcs make an S
    &::before,
    &::after {
      content: '';
      position: absolute;
      left: 0;
      width: 100%;
      height: 52%;
      border: 2.5px solid transparent;
      border-radius: 50%;
    }

    &::before {
      top: 0;
      border-left-color: var(--steam);
    }

    // shifted left so its top meets the bottom of the upper arc
    &::after {
      bottom: 0;
      left: -70%;
      border-right-color: var(--steam);
    }
  }
}

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

// 0% = strip facing the viewer; the lamp is front-left. Between 25% and 75% it faces away.
@keyframes d-coffeecup-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;
  }
}

// Rise, sway, grow and fade. Starts and ends invisible, so the jump back is never seen.
@keyframes d-coffeecup-steam {
  0% {
    opacity: 0;
    transform: translate(calc(var(--x) * 1px), 10px) scale(0.6);
  }

  30% {
    opacity: 0.9;
  }

  60% {
    transform: translate(calc(var(--x) * 1px + 4px), -12px) scale(0.9, 1.1);
  }

  100% {
    opacity: 0;
    transform: translate(calc(var(--x) * 1px - 2px), -34px) scale(1.1, 1.3);
  }
}
Standalone snippet · plain CSS, no build step, no dependencies.