Pure CSS Products & branding #loop #sass-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.

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

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.

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

.scene {
  perspective: 800px;
}

.bottle {
  --gold: #ffcf87;
  --gold-deep: #aa6c16;
  position: relative;
  width: 84px;
  height: 96px;
  transform-style: preserve-3d;
  animation: sway 8s ease-in-out infinite alternate;
}

.shadow {
  position: absolute;
  left: -18px;
  top: 56px;
  width: 120px;
  height: 80px;
  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((84px - var(--w)) / 2);
  width: var(--w);
  height: var(--h);
  transform-style: preserve-3d;
}

.glass  { --w: 84px; --d: 44px; --c: 9px; --h: 96px; top: 0; }
.liquid { --w: 74px; --d: 34px; --c: 6.93px; --h: 50px; top: 33px; } /* 5px glass walls, 13px 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, 0px)) 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, 84px);
  height: var(--d, 44px);
  --c: 9px;
  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: -22px;
  background: radial-gradient(ellipse 16px 11px, rgb(255 255 255 / 0.3), transparent), rgb(200 190 255 / 0.18);
  transform: rotateX(90deg);
}

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

/* glass: tinted, light edges, drawn from both sides; the lower 13px is the thick bottom */
.glass > i {
  --lit: 0.1;
  border: 1px solid rgb(236 238 251 / 0.38);
  border-radius: 2px;
  background:
    linear-gradient(transparent calc(100% - 14px), rgb(255 255 255 / 0.35) calc(100% - 13px), rgb(255 255 255 / 0.12) calc(100% - 10px), 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: 74px;
  --d: 34px;
  --c: 6.93px;
  top: -17px;
  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: 22px;
  height: 140%;
  background: linear-gradient(90deg, transparent, rgb(255 255 255 / 0.7) 45% 55%, transparent);
  transform: translateX(-40px) rotate(18deg);
  animation: glint 8s linear infinite;
}

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

.label b { font-size: 11px; font-weight: 400; letter-spacing: 2.5px; text-indent: 2.5px; }
.label small { font: 4.5px system-ui, sans-serif; letter-spacing: 0.8px; 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: 24px;
  --bd: 18px;
  left: 30px;
  top: -7px;
  width: 24px;
  height: 7px;
}

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

.cap {
  --bw: 42px;
  --bd: 30px;
  left: 21px;
  top: -39px;
  width: 42px;
  height: 32px;
}

/* vertical fluting over polished gold */
.cap > * {
  background:
    repeating-linear-gradient(90deg, rgb(255 255 255 / 0.22) 0 1.5px, transparent 1.5px 3px, rgb(0 0 0 / 0.12) 3px 4.5px, transparent 4.5px 6px),
    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 1.5px, transparent 1.5px 3px, rgb(0 0 0 / 0.14) 3px 4.5px, transparent 4.5px 6px),
    linear-gradient(90deg, var(--gold-deep), #e2a54f);
}
.cap > :nth-child(5) {
  background:
    radial-gradient(circle, transparent 0 7px, var(--gold-deep) 7.5px 8.5px, transparent 9px),
    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(-40px) rotate(18deg); }
  64%, 100% { transform: translateX(90px) rotate(18deg); }
}

Sass source 330 lines

@use 'sass:math';
@use 'sass:list';

// A faceted perfume bottle: an octagonal glass prism (a box with its four vertical edges cut off)
// with a smaller octagonal prism of liquid inside, a gold cap and a label.
$W: 84px; // body width
$D: 44px; // body depth
$H: 96px; // body height
$c: 9px; // how much each vertical edge is cut off
$g: 5px; // glass wall thickness
$base: 13px; // the thick glass bottom
$fill: 50px; // liquid height
$sway: 8s;

// Eight faces around the Y axis: front, corner, side, corner, back, corner, side, corner. Each
// face is turned to its angle and pushed out to its distance from the axis (half the depth,
// half the width, or the corner cut's distance).
@mixin octa-faces($w, $d, $c, $h) {
  $wide: $w - 2 * $c;
  $narrow: $d - 2 * $c;
  $cut: $c * math.sqrt(2);
  $cut-z: math.div($w * 0.5 + $d * 0.5 - $c, math.sqrt(2));

  // A corner cut faces 45deg, but when width != depth its middle is NOT on the 45deg ray from
  // the axis: it also slides sideways, by (w - d) / 2 / sqrt(2), alternately left and right.
  $slide: math.div(($w - $d) * 0.5, math.sqrt(2));
  // width, distance and slide for: front / back, a corner, a side, a corner (by $i % 4)
  $widths: ($wide, $cut, $narrow, $cut);
  $dists: ($d * 0.5, $cut-z, $w * 0.5, $cut-z);
  $slides: (0px, $slide, 0px, -$slide);

  @for $i from 0 to 8 {
    $a: $i * 45deg;
    $fw: list.nth($widths, $i % 4 + 1);
    $fz: list.nth($dists, $i % 4 + 1);
    $fx: list.nth($slides, $i % 4 + 1);

    > i:nth-child(#{$i + 1}) {
      left: ($w - $fw) * 0.5;
      width: $fw;
      transform: rotateY($a) translateX($fx) translateZ($fz);
    }
  }

  > i {
    position: absolute;
    top: 0;
    height: $h;
  }
}

// An octagon the size of the prism's footprint (for the lid, the floor, the liquid's surface).
@function octagon($w, $d, $c) {
  @return polygon($c 0, $w - $c 0, $w $c, $w $d - $c, $w - $c $d, $c $d, 0 $d - $c, 0 $c);
}

// Four walls and a lid, for the neck and the cap.
@mixin box-sides($w, $d, $h) {
  > * {
    position: absolute;
    top: 0;
    left: 0;
    width: $w;
    height: $h;
  }

  > :nth-child(1) {
    transform: translateZ($d * 0.5);
  }

  > :nth-child(2) {
    transform: rotateY(180deg) translateZ($d * 0.5);
  }

  > :nth-child(3),
  > :nth-child(4) {
    left: ($w - $d) * 0.5;
    width: $d;
  }

  > :nth-child(3) {
    transform: rotateY(90deg) translateZ($w * 0.5);
  }

  > :nth-child(4) {
    transform: rotateY(-90deg) translateZ($w * 0.5);
  }

  > :nth-child(5) {
    top: -$d * 0.5;
    height: $d;
    transform: rotateX(90deg);
  }
}

.d-perfume {
  --gold: color-mix(in srgb, var(--warm) 78%, #fff);
  --gold-deep: color-mix(in srgb, var(--warm) 62%, #3a2200);
  --edge: color-mix(in srgb, var(--text) 38%, transparent);
  position: relative;
  width: $W;
  height: $H;
  transform-style: preserve-3d;
  animation: d-perfume-sway $sway ease-in-out infinite alternate;

  &__shadow {
    position: absolute;
    left: -18px;
    top: $H - 40px;
    width: $W + 36px;
    height: 80px;
    border-radius: 50%;
    background: radial-gradient(closest-side, color-mix(in srgb, var(--hot) 30%, rgb(0 0 0 / 0.45)), transparent);
    transform: rotateX(90deg);
  }

  // --- glass -------------------------------------------------------------------------------
  // Faces are see-through and drawn from both sides, so the far walls show through the near
  // ones. The lower $base px of every wall is the thick, clearer glass bottom.
  &__glass {
    position: absolute;
    inset: 0;
    transform-style: preserve-3d;
    @include octa-faces($W, $D, $c, $H);

    > i {
      --lit: 0.1;
      border: 1px solid var(--edge);
      border-radius: 2px;
      background:
        linear-gradient(transparent calc(100% - #{$base + 1px}), rgb(255 255 255 / 0.35) calc(100% - #{$base}), rgb(255 255 255 / 0.12) calc(100% - #{$base - 3px}), 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))),
        color-mix(in srgb, var(--accent) 8%, transparent);
    }

    // the corner cuts catch the light
    > i:nth-child(2n) {
      --lit: 0.24;
    }

    > i:nth-child(3),
    > i:nth-child(7) {
      --lit: 0.05;
    }

    // front wall: carries the sweeping glint (overflow is fine, this face is flat)
    > i:first-child {
      overflow: hidden;
    }
  }

  &__glint {
    position: absolute;
    top: -20%;
    left: 0;
    width: 22px;
    height: 140%;
    background: linear-gradient(90deg, transparent, rgb(255 255 255 / 0.7) 45% 55%, transparent);
    transform: translateX(-40px) rotate(18deg);
    animation: d-perfume-glint $sway linear infinite;
  }

  &__top,
  &__floor {
    position: absolute;
    left: 0;
    top: -$D * 0.5;
    width: $W;
    height: $D;
    clip-path: octagon($W, $D, $c);
    background:
      radial-gradient(ellipse 16px 11px, rgb(255 255 255 / 0.3), transparent),
      color-mix(in srgb, var(--accent) 10%, rgb(255 255 255 / 0.14));
    transform: rotateX(90deg);
  }

  &__floor {
    top: $H - $D * 0.5;
    background: color-mix(in srgb, var(--accent) 16%, rgb(255 255 255 / 0.1));
    transform: rotateX(-90deg);
  }

  // --- liquid: the same prism, smaller, standing on the thick bottom ---------------------------
  &__liquid {
    $lw: $W - 2 * $g;
    $ld: $D - 2 * $g;
    $lc: $c - $g * (math.sqrt(2) - 1); // a parallel cut, $g further in
    position: absolute;
    left: $g;
    top: $H - $base - $fill;
    width: $lw;
    height: $fill;
    transform-style: preserve-3d;
    @include octa-faces($lw, $ld, $lc, $fill);

    > i {
      background:
        linear-gradient(90deg, rgb(255 255 255 / 0.18), transparent 35% 70%, rgb(0 0 0 / 0.18)),
        linear-gradient(color-mix(in srgb, var(--hot) 55%, var(--warm)), color-mix(in srgb, var(--hot) 80%, var(--accent)));
      opacity: 0.82;
    }

    // the surface of the liquid
    b {
      position: absolute;
      left: 0;
      top: -$ld * 0.5;
      width: $lw;
      height: $ld;
      clip-path: octagon($lw, $ld, $lc);
      background:
        radial-gradient(ellipse 60% 70% at 40% 40%, rgb(255 255 255 / 0.45), transparent),
        color-mix(in srgb, var(--hot) 45%, var(--warm));
      opacity: 0.9;
      transform: rotateX(90deg);
    }
  }

  // --- label -------------------------------------------------------------------------------
  &__label {
    position: absolute;
    left: $c + 4px;
    top: 26px;
    display: grid;
    place-items: center;
    align-content: center;
    gap: 3px;
    width: $W - 2 * $c - 8px;
    height: 36px;
    border: 1px solid var(--gold);
    background: #0d0e17;
    box-shadow: inset 0 0 0 2px #0d0e17, inset 0 0 0 2.6px var(--gold-deep);
    color: var(--gold);
    font-family: Georgia, 'Times New Roman', serif;
    line-height: 1;
    backface-visibility: hidden;
    transform: translateZ($D * 0.5 + 0.6px);

    b {
      font-size: 11px;
      font-weight: 400;
      letter-spacing: 2.5px;
      text-indent: 2.5px; // letter-spacing adds space after the last letter too; this rebalances it
    }

    small {
      font-family: system-ui, sans-serif;
      font-size: 4.5px;
      letter-spacing: 0.8px;
      white-space: nowrap;
      opacity: 0.85;
    }
  }

  // --- neck and cap ------------------------------------------------------------------------
  &__neck {
    position: absolute;
    left: ($W - 24px) * 0.5;
    top: -7px;
    width: 24px;
    height: 7px;
    transform-style: preserve-3d;
    @include box-sides(24px, 18px, 7px);

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

  &__cap {
    position: absolute;
    left: ($W - 42px) * 0.5;
    top: -7px - 32px;
    width: 42px;
    height: 32px;
    transform-style: preserve-3d;
    @include box-sides(42px, 30px, 32px);

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

    > :nth-child(1) {
      overflow: hidden;
    }

    > :nth-child(3),
    > :nth-child(4) {
      background:
        repeating-linear-gradient(90deg, rgb(255 255 255 / 0.18) 0 1.5px, transparent 1.5px 3px, rgb(0 0 0 / 0.14) 3px 4.5px, transparent 4.5px 6px),
        linear-gradient(90deg, var(--gold-deep), color-mix(in srgb, var(--gold) 70%, var(--gold-deep)));
    }

    > :nth-child(5) {
      background:
        radial-gradient(circle, transparent 0 7px, var(--gold-deep) 7.5px 8.5px, transparent 9px),
        linear-gradient(135deg, #fff6dc, var(--gold) 45%, var(--gold-deep));
    }

    .d-perfume__glint {
      animation-delay: $sway * -0.06;
    }
  }
}

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

  to {
    transform: rotateX(-12deg) rotateY(38deg);
  }
}

// One sweep per half swing, while the bottle passes the front (the middle of each half).
@keyframes d-perfume-glint {
  0%,
  36% {
    transform: translateX(-40px) rotate(18deg);
  }

  64%,
  100% {
    transform: translateX(90px) rotate(18deg);
  }
}
Standalone snippet · plain CSS, no build step, no dependencies.