Pure CSS Products & branding #loop #product #glass #luxury

Perfume bottle in pure CSS

A faceted glass bottle you can see into: an octagonal prism of glass around a smaller prism of liquid, a fluted gold cap and a label. A glint sweeps across each time it faces you.

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

How it works

  1. The body is an octagonal prism: a box with its four vertical edges cut off. Eight faces are turned in 45° steps with rotateY(n × 45deg) and pushed out to their distance from the axis.
  2. The corner cuts are the subtle part. When the box is wider than it is deep, the middle of a cut is not on the 45° ray from the axis, so it also slides sideways by (w − d) / 2 / √2, alternately left and right.
  3. Every size comes from four custom properties (--w --d --c --h), so the liquid is simply the same prism again, a glass thickness smaller, standing on the thick glass bottom.
  4. Glass is see-through: its faces are tinted, have light edges and are drawn from both sides, so the far walls and the liquid show through the near ones. Top, floor and liquid surface are octagons cut with clip-path.
  5. The glint is a bright gradient inside the front face (a flat face can clip with overflow: hidden), moved with translateX while the bottle passes the front.
  6. Every length is a multiple of one base unit, --u, tied to the canvas, so the bottle is the same share of a gallery card, the editor and a recording canvas.

Key techniques

  • octagonal prism: 8 faces at 45° steps
  • see-through faces drawn from both sides
  • a prism inside a prism for the liquid
  • glint = translated gradient in a clipped face

Copy-paste code

HTML 31 lines

<div class="scene">
  <div class="bottle">
    <i class="shadow"></i>
    <div class="prism liquid">
      <i style="--n:0"></i>
      <i style="--n:1"></i>
      <i style="--n:2"></i>
      <i style="--n:3"></i>
      <i style="--n:4"></i>
      <i style="--n:5"></i>
      <i style="--n:6"></i>
      <i style="--n:7"></i>
      <b class="octagon surface"></b>
    </div>
    <b class="octagon floor"></b>
    <div class="prism glass">
      <i style="--n:0"><em class="glint"></em></i>
      <i style="--n:1"></i>
      <i style="--n:2"></i>
      <i style="--n:3"></i>
      <i style="--n:4"></i>
      <i style="--n:5"></i>
      <i style="--n:6"></i>
      <i style="--n:7"></i>
    </div>
    <b class="octagon top"></b>
    <span class="label"><b>LUMEN</b><small>EAU DE PARFUM</small></span>
    <div class="box neck"><i></i><i></i><i></i><i></i></div>
    <div class="box cap"><i><em class="glint"></em></i><i></i><i></i><i></i><i></i></div>
  </div>
</div>

CSS 224 lines

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

.bottle {
  --gold: #ffcf87;
  --gold-deep: #aa6c16;
  position: relative;
  width: calc(84 * var(--u));
  height: calc(96 * var(--u));
  transform-style: preserve-3d;
  /* the neck and the cap stand on top of this box, so the drawing's middle is above the box's:
     move the box down by that much, and the whole bottle is centred */
  translate: 0 calc(17 * var(--u));
  animation: sway 8s ease-in-out infinite alternate;
}

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

/* --- the octagonal prism: width, depth, corner cut, height --- */
.prism {
  position: absolute;
  left: calc((84 * var(--u) - var(--w)) / 2);
  width: var(--w);
  height: var(--h);
  transform-style: preserve-3d;
}

.glass  { --w: calc(84 * var(--u)); --d: calc(44 * var(--u)); --c: calc(9 * var(--u)); --h: calc(96 * var(--u)); top: 0; }
.liquid { --w: calc(74 * var(--u)); --d: calc(34 * var(--u)); --c: calc(6.93 * var(--u)); --h: calc(50 * var(--u)); top: calc(33 * var(--u)); } /* 5 units glass walls, 13 units glass floor */

.prism > i {
  position: absolute;
  top: 0;
  box-sizing: border-box;
  left: calc((var(--w) - var(--fw)) / 2);
  width: var(--fw);
  height: var(--h);
  transform: rotateY(calc(var(--n) * 45deg)) translateX(var(--fx, 0)) translateZ(var(--fz));
}

/* front and back */
.prism > i:nth-child(4n + 1) { --fw: calc(var(--w) - 2 * var(--c)); --fz: calc(var(--d) / 2); }
/* the two sides */
.prism > i:nth-child(4n + 3) { --fw: calc(var(--d) - 2 * var(--c)); --fz: calc(var(--w) / 2); }
/* the four corner cuts: 45deg, but also slid sideways when w != d */
.prism > i:nth-child(even) {
  --fw: calc(var(--c) * 1.4142);
  --fz: calc((var(--w) / 2 + var(--d) / 2 - var(--c)) / 1.4142);
}
.prism > i:nth-child(4n + 2) { --fx: calc((var(--w) - var(--d)) / 2 / 1.4142); }
.prism > i:nth-child(4n)     { --fx: calc((var(--d) - var(--w)) / 2 / 1.4142); }

/* an octagon of the prism's footprint, laid flat */
.octagon {
  position: absolute;
  left: 0;
  width: var(--w, calc(84 * var(--u)));
  height: var(--d, calc(44 * var(--u)));
  --c: calc(9 * var(--u));
  clip-path: polygon(var(--c) 0, calc(100% - var(--c)) 0, 100% var(--c), 100% calc(100% - var(--c)),
    calc(100% - var(--c)) 100%, var(--c) 100%, 0 calc(100% - var(--c)), 0 var(--c));
}

.top {
  top: calc(-22 * var(--u));
  background: radial-gradient(ellipse calc(16 * var(--u)) calc(11 * var(--u)), rgb(255 255 255 / 0.3), transparent), rgb(200 190 255 / 0.18);
  transform: rotateX(90deg);
}

.floor {
  top: calc(74 * var(--u));
  background: rgb(190 175 255 / 0.2);
  transform: rotateX(-90deg);
}

/* glass: tinted, light edges, drawn from both sides; the lower 13 units is the thick bottom */
.glass > i {
  --lit: 0.1;
  /* an inner line plus a soft shade instead of a hard border: swaying, the corner cuts turn
     nearly side-on, where a 1 units line breaks up; the shade survives */
  box-shadow: inset 0 0 0 calc(1 * var(--u)) rgb(236 238 251 / 0.38), inset 0 0 calc(6 * var(--u)) rgb(236 238 251 / 0.15);
  border-radius: calc(2 * var(--u));
  background:
    linear-gradient(transparent calc(100% - 15 * var(--u)), rgb(255 255 255 / 0.35) calc(100% - 14 * var(--u)), rgb(255 255 255 / 0.12) calc(100% - 11 * var(--u)), rgb(255 255 255 / 0.2)),
    linear-gradient(90deg, rgb(255 255 255 / calc(var(--lit) * 1.8)), rgb(255 255 255 / var(--lit)) 30%, rgb(255 255 255 / calc(var(--lit) * 0.5)) 70%, rgb(255 255 255 / var(--lit))),
    rgb(139 108 255 / 0.08);
}

.glass > i:nth-child(even) { --lit: 0.24; } /* the corner cuts catch the light */
.glass > i:nth-child(3),
.glass > i:nth-child(7) { --lit: 0.05; }
.glass > i:first-child { overflow: hidden; } /* flat face: clipping is safe */

.liquid > i {
  background:
    linear-gradient(90deg, rgb(255 255 255 / 0.18), transparent 35% 70%, rgb(0 0 0 / 0.18)),
    linear-gradient(#ff7a78, #f0569f);
  opacity: 0.82;
}

.surface {
  --w: calc(74 * var(--u));
  --d: calc(34 * var(--u));
  --c: calc(6.93 * var(--u));
  top: calc(-17 * var(--u));
  background: radial-gradient(ellipse 60% 70% at 40% 40%, rgb(255 255 255 / 0.45), transparent), #ff8a70;
  opacity: 0.9;
  transform: rotateX(90deg);
}

.glint {
  position: absolute;
  top: -20%;
  left: 0;
  width: calc(22 * var(--u));
  height: 140%;
  background: linear-gradient(90deg, transparent, rgb(255 255 255 / 0.7) 45% 55%, transparent);
  transform: translateX(calc(-40 * var(--u))) rotate(18deg);
  animation: glint 8s linear infinite;
}

.label {
  position: absolute;
  left: calc(13 * var(--u));
  top: calc(26 * var(--u));
  display: grid;
  place-items: center;
  align-content: center;
  gap: calc(3 * var(--u));
  box-sizing: border-box;
  width: calc(58 * var(--u));
  height: calc(36 * var(--u));
  border: calc(1 * var(--u)) solid var(--gold);
  background: #0d0e17;
  box-shadow: inset 0 0 0 calc(2 * var(--u)) #0d0e17, inset 0 0 0 calc(2.6 * var(--u)) var(--gold-deep);
  color: var(--gold);
  font-family: Georgia, 'Times New Roman', serif;
  line-height: 1;
  backface-visibility: hidden;
  transform: translateZ(calc(22.6 * var(--u))); /* just in front of the glass */
}

.label b { font-size: calc(11 * var(--u)); font-weight: 400; letter-spacing: calc(2.5 * var(--u)); text-indent: calc(2.5 * var(--u)); }
.label small { font: calc(4.5 * var(--u)) Inter, system-ui, sans-serif; letter-spacing: calc(0.8 * var(--u)); white-space: nowrap; opacity: 0.85; }

/* --- neck and cap: four walls and a lid --- */
.box {
  position: absolute;
  transform-style: preserve-3d;
}

.box > * { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
.box > :nth-child(1) { transform: translateZ(calc(var(--bd) / 2)); }
.box > :nth-child(2) { transform: rotateY(180deg) translateZ(calc(var(--bd) / 2)); }
.box > :nth-child(3),
.box > :nth-child(4) { left: calc(50% - var(--bd) / 2); width: var(--bd); }
.box > :nth-child(3) { transform: rotateY(90deg) translateZ(calc(var(--bw) / 2)); }
.box > :nth-child(4) { transform: rotateY(-90deg) translateZ(calc(var(--bw) / 2)); }
.box > :nth-child(5) { top: calc(var(--bd) / -2); height: var(--bd); transform: rotateX(90deg); }

.neck {
  --bw: calc(24 * var(--u));
  --bd: calc(18 * var(--u));
  left: calc(30 * var(--u));
  top: calc(-7 * var(--u));
  width: calc(24 * var(--u));
  height: calc(7 * var(--u));
}

.neck > * { background: linear-gradient(var(--gold-deep), var(--gold) 50%, var(--gold-deep)); }

.cap {
  --bw: calc(42 * var(--u));
  --bd: calc(30 * var(--u));
  left: calc(21 * var(--u));
  top: calc(-39 * var(--u));
  width: calc(42 * var(--u));
  height: calc(32 * var(--u));
}

/* vertical fluting over polished gold */
.cap > * {
  background:
    repeating-linear-gradient(90deg, rgb(255 255 255 / 0.22) 0 calc(1.5 * var(--u)), transparent calc(1.5 * var(--u)) calc(3 * var(--u)), rgb(0 0 0 / 0.12) calc(3 * var(--u)) calc(4.5 * var(--u)), transparent calc(4.5 * var(--u)) calc(6 * var(--u))),
    linear-gradient(90deg, var(--gold-deep), var(--gold) 40%, #fff6dc 50%, var(--gold) 60%, var(--gold-deep));
}

.cap > :nth-child(1) { overflow: hidden; }
.cap > :nth-child(3),
.cap > :nth-child(4) {
  background:
    repeating-linear-gradient(90deg, rgb(255 255 255 / 0.18) 0 calc(1.5 * var(--u)), transparent calc(1.5 * var(--u)) calc(3 * var(--u)), rgb(0 0 0 / 0.14) calc(3 * var(--u)) calc(4.5 * var(--u)), transparent calc(4.5 * var(--u)) calc(6 * var(--u))),
    linear-gradient(90deg, var(--gold-deep), #e2a54f);
}
.cap > :nth-child(5) {
  background:
    radial-gradient(circle, transparent 0 calc(7 * var(--u)), var(--gold-deep) calc(7.5 * var(--u)) calc(8.5 * var(--u)), transparent calc(9 * var(--u))),
    linear-gradient(135deg, #fff6dc, var(--gold) 45%, var(--gold-deep));
}
.cap .glint { animation-delay: -0.5s; }

@keyframes sway {
  from { transform: rotateX(-12deg) rotateY(-38deg); }
  to   { transform: rotateX(-12deg) rotateY(38deg); }
}

/* one sweep per half swing, while the bottle passes the front */
@keyframes glint {
  0%, 36%   { transform: translateX(calc(-40 * var(--u))) rotate(18deg); }
  64%, 100% { transform: translateX(calc(90 * var(--u))) rotate(18deg); }
}
Standalone snippet · plain CSS, no build step, no dependencies.