Pure CSS Products & branding #loop #product #medal #award #coin

Award medal in pure CSS

A medal hanging from a neck ribbon: it sways from the neck and twists on its ring, with a real metal edge and a shine that sweeps each time it faces you.

  • 155 lines of CSS
  • 13 lines of HTML
  • No JavaScript
  • No dependencies
  • Snippet under MIT
GitHub

How it works

  1. Build it the way a real medal hangs: two ribbon straps meet in a folded tab, and the medal has its own ring (the bail) whose top loop sits just behind the tab. Ribbon and medal read as one object.
  2. The whole piece sways from the neck: transform-origin: 50% 0 on a wrapper and a small rotateZ swing, alternate.
  3. The medal twists on its ring between rotateY(-50deg) and rotateY(50deg), never edge-on, while the bail stays flat: as a child of the medal it runs the opposite twist, which cancels it, so it never swings through the ribbon.
  4. The edge is five discs 1.2 units apart behind the face, so a turned medal shows a band of metal. The shine runs on the same 5s cycle as the twist, so each sweep is centred on the moment the face looks at you.
  5. Every length is a multiple of one base unit, --u, tied to the canvas, so the medal is the same share of a gallery card, the editor and a recording canvas.

Key techniques

  • bail looped behind a folded tab
  • pendulum from transform-origin: top
  • edge from stacked discs
  • shine synced to the twist

Copy-paste code

HTML 13 lines

<div class="scene">
  <div class="badge">
    <div class="swing">
      <i class="strap"></i><i class="strap"></i>
      <div class="medal">
        <i class="bail"></i>
        <u style="--i:0"></u><u style="--i:1"></u><u style="--i:2"></u><u style="--i:3"></u><u style="--i:4"></u>
        <div class="face"><b></b><small>WINNER</small><em></em></div>
      </div>
      <i class="tab"></i>
    </div>
  </div>
</div>

CSS 155 lines

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

.badge {
  --gold: #ffd28f;
  --gold-deep: #aa7519;
  position: relative;
  width: calc(120 * var(--u));
  height: calc(196 * var(--u));
  transform-style: preserve-3d;
}

/* sways from the top, like something hanging from a neck */
.swing {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  transform-origin: 50% 0;
  animation: sway 2.5s ease-in-out infinite alternate;
}

/* two straps pinned at the tab, splayed toward the neck, slightly behind everything */
.strap {
  position: absolute;
  top: 0;
  left: calc(50% - 12 * var(--u));
  width: calc(24 * var(--u));
  height: calc(92 * var(--u));
  background:
    linear-gradient(90deg, rgb(0 0 0 / 0.28), transparent 30% 70%, rgb(0 0 0 / 0.28)),
    linear-gradient(90deg, #8b6cff 0 30%, #fff 30% 36%, #ff4d9d 36% 64%, #fff 64% 70%, #8b6cff 70%);
  transform-origin: 50% 100%;
  transform: translateX(calc(-7 * var(--u))) translateZ(calc(-3 * var(--u))) rotate(-20deg);
}

.strap + .strap {
  transform: translateX(calc(7 * var(--u))) translateZ(calc(-2 * var(--u))) rotate(20deg);
}

/* the folded end of the ribbon, in front of the bail so the bail looks looped through it */
.tab {
  position: absolute;
  top: calc(80 * var(--u));
  left: calc(50% - 19 * var(--u));
  width: calc(38 * var(--u));
  height: calc(14 * var(--u));
  border-radius: calc(3 * var(--u));
  background:
    linear-gradient(rgb(255 255 255 / 0.25), transparent 40%, rgb(0 0 0 / 0.3)),
    linear-gradient(90deg, #8b6cff, #ff4d9d 50%, #8b6cff);
  box-shadow: 0 calc(2 * var(--u)) calc(4 * var(--u)) rgb(0 0 0 / 0.35);
  transform: translateZ(calc(2 * var(--u)));
}

.medal {
  position: absolute;
  top: calc(100 * var(--u));
  left: calc(50% - 48 * var(--u));
  width: calc(96 * var(--u));
  height: calc(96 * var(--u));
  transform-style: preserve-3d;
  animation: twist 5s ease-in-out infinite alternate;
}

/* the edge: five discs 1.2 units apart */
.medal u {
  position: absolute;
  inset: calc(0.5 * var(--u));
  border-radius: 50%;
  background: linear-gradient(90deg, var(--gold-deep), var(--gold) 50%, var(--gold-deep));
  transform: translateZ(calc((var(--i) - 2) * 1.2 * var(--u)));
}

/* the bail hangs flat from the tab while the medal turns on it: as the medal's child it runs
   the opposite twist with the same timing, which cancels it (turning with the medal would swing
   it through the tab and show a hard cut) */
.bail {
  position: absolute;
  top: calc(-16 * var(--u));
  left: calc(50% - 9 * var(--u));
  width: calc(18 * var(--u));
  height: calc(20 * var(--u));
  box-sizing: border-box;
  border: calc(3 * var(--u)) solid var(--gold);
  border-radius: 50%;
  box-shadow: inset 0 0 0 calc(1 * var(--u)) var(--gold-deep), 0 0 0 calc(1 * var(--u)) var(--gold-deep);
  animation: untwist 5s ease-in-out infinite alternate;
}

.face {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  align-content: center;
  gap: calc(2 * var(--u));
  overflow: hidden; /* fine: the face itself is flat */
  border-radius: 50%;
  background:
    radial-gradient(circle at 32% 26%, rgb(255 255 255 / 0.55), transparent 42%),
    conic-gradient(from 20deg, var(--gold), var(--gold-deep), var(--gold), #c89445, var(--gold));
  box-shadow:
    inset 0 0 0 calc(4 * var(--u)) #ffe3b8,
    inset 0 0 0 calc(6 * var(--u)) var(--gold-deep),
    inset 0 0 0 calc(12 * var(--u)) rgb(170 117 25 / 0.25);
  color: #5a3a10;
  font-family: Inter, system-ui, sans-serif;
  transform: translateZ(calc(3 * var(--u)));
}

.face b {
  width: calc(40 * var(--u));
  height: calc(40 * var(--u));
  clip-path: polygon(50% 0, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
  background: linear-gradient(160deg, #fff8de, var(--gold) 45%, var(--gold-deep));
}

.face small {
  font-size: calc(8 * var(--u));
  font-weight: 800;
  letter-spacing: calc(2 * var(--u));
}

/* one sweep per twist, centred on the moment the face looks straight at you */
.face em {
  position: absolute;
  inset: 0;
  background: linear-gradient(115deg, transparent 38%, rgb(255 255 255 / 0.7) 50%, transparent 62%);
  transform: translateX(-120%);
  animation: shine 5s linear infinite;
}

@keyframes sway {
  from { transform: rotateZ(-3deg); }
  to   { transform: rotateZ(3deg); }
}

@keyframes twist {
  from { transform: rotateY(-50deg); }
  to   { transform: rotateY(50deg); }
}

@keyframes untwist {
  from { transform: rotateY(50deg); }
  to   { transform: rotateY(-50deg); }
}

@keyframes shine {
  0%, 35%   { transform: translateX(-120%); }
  65%, 100% { transform: translateX(120%); }
}
Standalone snippet · plain CSS, no build step, no dependencies.