Frontend Development 10 min read

How to Create a Rotating 3D Coin Animation with CSS3, LESS and React

This article explains how to build a rotating 3D coin effect for a web page using CSS3 animations, @keyframes, LESS variables and loops, and a React component that generates the coin's faces and edges, providing a visual cue to attract users to an activity.

ByteFE
ByteFE
ByteFE
How to Create a Rotating 3D Coin Animation with CSS3, LESS and React

Recently the product team asked to add a rotating coin in a corner of a page to attract users to an activity, with a very tight deadline and no design ready.

The simplest idea is to use a GIF inside an <a> tag, but we chose a pure CSS solution.

The rotation itself can be done with CSS3 animation and @keyframes:

.coin {
  transform-style: preserve-3d;
  animation: rotate3d 3s linear infinite;
}

@keyframes rotate3d {
  0% {
    transform: perspective(1000px) rotateY(0deg);
  }
  100% {
    transform: perspective(1000px) rotateY(360deg);
  }
}

To create the coin faces we use two div elements. By removing the surface pattern the coin becomes a very thin cylinder, so we need a front, a back and a curved side.

The front and back are straightforward:

$coin-thickness: 4px;
$coin-front: "https://tosv.byted.org/obj/maat/img/d2FuZ3NoaXpoZW4uNzUzMA/file_178a58ae02711.png";
$coin-back: "https://tosv.byted.org/obj/maat/img/d2FuZ3NoaXpoZW4uNzUzMA/file_178a58ae0276.png";

.coin__front {
  background-image: url($coin-front);
  background-size: cover;
  transform: translateZ($coin-thickness / 2);
}

.coin__back {
  background-image: url($coin-back);
  background-size: cover;
  transform: translateZ(-$coin-thickness / 2) rotateY(180deg);
}

The side of the coin cannot be drawn with a single CSS property; we approximate it with a many‑sided polygon (a “multi‑prism”). The number of sides determines how convincing the illusion is.

We generate the required div elements with a LESS @for loop (or a JavaScript Array.map ) and then position each side using translate and rotate transforms.

const n = 80;
{Array(n).fill(1).map((value, key) => (
))}

The SCSS for each edge calculates its height, width and the series of transforms needed to place it around the coin:

// size of the prism (diameter)
$coin-diameter: 320px;
// thickness of the coin
$coin-thickness: 40px;
// colour of the coin
$coin-color: rgb(255, 223, 95);
// number of faces
$edge-faces: 100;
// length of each face
$edge-face-length: 3.14 * $coin-diameter / $edge-faces;

.coin__edge {
  > div {
    position: absolute;
    height: $edge-face-length;
    width: $coin-thickness;
  }
  @for $i from 1 through $edge-faces {
    div:nth-child(#{$i}) {
      background: $coin-color;
      transform:
        translateY($coin-diameter / 2 - $edge-face-length / 2)
        translateX($coin-diameter / 2 - $coin-thickness / 2)
        rotateY(90deg)
        rotateX(360deg / $edge-faces * $i + 90)
        translateZ($coin-diameter / 2);
    }
  }
}

Images below illustrate the steps: default position, moving to centre, horizontal rotation, adjusting each facet’s angle, and finally moving the facet to the edge of the prism.

Finally the complete code is provided, including the React component skeleton, all LESS variables, the styles for .coin , .coin__front , .coin__back , .coin__edge , the shadow element, and the two keyframe animations ( rotate3d and shine ).

const CoinTrans = () => {
  return (
{Array(100).fill(1).map((item, key) => (
))}
{/* shadow */}
);
};

$coin-diameter: 320px;
$coin-thickness: 40px;
$coin-color: rgb(255, 223, 95);
$edge-faces: 100;
$edge-face-length: 3.14 * $coin-diameter / $edge-faces;

.coin {
  position: relative;
  width: $coin-diameter;
  height: $coin-diameter;
  margin: 0 auto;
  transform-style: preserve-3d;
  animation: rotate3d 8s linear infinite;
  transition: all 0.3s;
}

.coin__front, .coin__back {
  position: absolute;
  width: $coin-diameter;
  height: $coin-diameter;
  border-radius: 50%;
  overflow: hidden;
  background-color: $coin-color;
  color: #000;
  font-size: $coin-diameter * 0.6;
  /* shine effect */
  &:after {
    content: "";
    position: absolute;
    left: -$coin-diameter / 2;
    bottom: 100%;
    height: $coin-diameter * 0.6;
    width: $coin-diameter * 2;
    background: #fff;
    opacity: 0.3;
    animation: shine linear 4s / 2 infinite;
  }
}

.coin__front {
  background-image: url($coin-front);
  background-size: cover;
  transform: translateZ($coin-thickness / 2);
}

.coin__back {
  background-image: url($coin-back);
  background-size: cover;
  transform: translateZ(-$coin-thickness / 2) rotateY(180deg);
}

.coin__edge {
  > div {
    position: absolute;
    height: $edge-face-length;
    width: $coin-thickness;
  }
  @for $i from 1 through $edge-faces {
    div:nth-child(#{$i}) {
      background: $coin-color;
      transform:
        translateY($coin-diameter / 2 - $edge-face-length / 2)
        translateX($coin-diameter / 2 - $coin-thickness / 2)
        rotateY(90deg)
        rotateX(360deg / $edge-faces * $i + 90)
        translateZ($coin-diameter / 2);
    }
  }
}

.coin__shadow {
  position: absolute;
  width: $coin-diameter;
  height: $coin-thickness;
  border-radius: 50%;
  background: #000;
  box-shadow: 0 0 $coin-thickness * 5 $coin-thickness * 5 #000;
  opacity: 0.125;
  transform: rotateX(90deg) translateZ(-$coin-diameter * 1.1) scale(0.5);
}

@keyframes rotate3d {
  0% { transform: perspective(1000px) rotateY(0deg); }
  100% { transform: perspective(1000px) rotateY(360deg); }
}

@keyframes shine {
  0%, 15% { transform: translateY($coin-diameter * 2) rotate(-40deg); }
  50% { transform: translateY(-$coin-diameter) rotate(-40deg); }
}

The data platform front‑end team is responsible for big‑data products such as Wind‑God, TEA, Libra and Dorado, and we are passionate about front‑end technology, data visualisation, massive data optimisation, web‑excel, WebIDE, private deployment and engineering tools.

animationReactCSS33DcoinLess
ByteFE
Written by

ByteFE

Cutting‑edge tech, article sharing, and practical insights from the ByteDance frontend team.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.