JSFiddle - React, Tailwind, and code Playground

by Amens

HTML

<!-- Javascript is used to set the content in order to reduce noise and show the core HTML structure -->
<div id="demo" class="flip-demo perspective-container">
<h1>
Flip Test
</h1>
  <div class="card">
    <section class="front face" aria-hidden="false"></section>
    <section class="back face" aria-hidden="true"></section>
    <!-- NEW! We need divs that represent the corners. -->
    <div class="top-right corner">
      <div style="--i: 0;"></div>
      <div style="--i: 1;"></div>
      <div style="--i: 2;"></div>
    </div>
    <div class="bottom-right corner">
      <div style="--i: 0;"></div>
      <div style="--i: 1;"></div>
      <div style="--i: 2;"></div>
    </div>
    <div class="bottom-left corner">
      <div style="--i: 0;"></div>
      <div style="--i: 1;"></div>
      <div style="--i: 2;"></div>
    </div>
    <div class="top-left corner">
      <div style="--i: 0;"></div>
      <div style="--i: 1;"></div>
      <div style="--i: 2;"></div>
    </div>

    <div class="top edge"></div>
    <div class="right edge"></div>
    <div class="bottom edge"></div>
    <div class="left edge"></div>
  </div>
  <button>Flip</button>
</div>

CSS

.card {
  --corner-granularity: 3; /* The number of faces used to simulate a round corner. More faces means more smooth. */
  --border-radius: 1.5em; /* exaggerated */
  border-radius: var(--border-radius);
}

.corner > * {
  background-color: red; /* Red is just to highlight the corners */
  /* background-color: blue; */
}

/* We have to override the edges so they do not overlap the corners */
.right, .left {
  inset-block: var(--border-radius);
  height: calc(100% - 2 * var(--border-radius));
} .top, .bottom {
  inset-inline: var(--border-radius);
  width: calc(100% - 2 * var(--border-radius));
}

.corner {
  --n: var(--corner-granularity);
  --r: var(--border-radius);
  
  position: absolute;
  transform-style: preserve-3d;
}

/* A corner is composed of a finite number of flat faces that, when arranged in just the right way, looks rounded. We need to do it this way because curved 3D surfaces do not exist in CSS. */
.corner > * {
  position: absolute;
  inset-block-end: 0;
  width: var(--card-depth);
  height: calc(2 * var(--r) * sin(45deg / var(--n)));
  transform-origin: bottom center;
  
  /* This math constructs a single corner. I derived it on a paper somewhere and threw it away, so you'll have to derive it yourself if you want to understand what's happening (: */
  transform:
    translateZ(calc(var(--r) * cos(var(--i) * 90deg / var(--n))))
    translateY(calc(-1 * var(--r) * sin(var(--i) * 90deg / var(--n))))
    rotateX(calc(45deg * (2 * var(--i) + 1) / var(--n)));
}

/* The rest of this code slots the corners where they belong. */
.top-right {
  inset-block-start: 0;
  inset-inline-end: 0;

  transform:
    rotateY(90deg)
    translateZ(calc(-1 * var(--r)))
    translateY(var(--r));
} .bottom-right {
  inset-block-end: 0;
  inset-inline-end: 0;

  transform:
    rotateY(90deg)
    rotateX(270deg)
    translateZ(calc(-1 * var(--r)))
    translateY(var(--r));
} .bottom-left {
  inset-block-end: 0;
  inset-inline-start: 0;

  transform:
   ...

JavaScript

function flipCard(card) {
  card.classList.toggle("facedown")
  const isFacedown = card.classList.contains("facedown")

  card.style.animationName = isFacedown
    ? "flip-to-back"
    : "flip-to-front"
  
  setAccessibility(card)
}

function setAccessibility(card) {
  const isFacedown = card.classList.contains("facedown")
  const front = card.querySelector(".front")
  const back = card.querySelector(".back")
  
  front.setAttribute("aria-hidden", isFacedown.toString())
  back.setAttribute("aria-hidden", (!isFacedown).toString())
}

/**
 * Just setup. Not important for the demo.
 */
const demo = document.querySelector("#demo")

function setupButton() {
  const button = demo.querySelector("button")
  const card = demo.querySelector(".card")
  
  button.addEventListener("click", () => flipCard(card))
}

setupButton()