<!-- Javascript is used to set the content in order to reduce noise and show the core HTML structure -->
<article id="demo" class="flip-demo perspective-container">
  <h2>Level 1: Verticality</h2>
  <div class="card">
    <section class="front face" aria-hidden="false"></section>
    <section class="back face" aria-hidden="true"></section>
  </div>
  <button>Flip Pichu</button>
</article>
@keyframes flip-to-front {
  0% { transform: translateZ(0em) rotateY(-180deg); }
  50% { transform: translateZ(var(--flip-height)) rotateY(-270deg); }
  100% { transform: translateZ(0em) rotateY(-360deg); }
}

/* I'm using a second animation for two reasons: 
 *  1. It allows the card to always flip in one direction.
 *  2. The renderer only plays an animation if it changes.
 */
@keyframes flip-to-back {
  0% { transform: translateZ(0em) rotateY(0deg); }
  50% { transform: translateZ(var(--flip-height)) rotateY(-90deg); }
  100% { transform: translateZ(0em) rotateY(-180deg); }
}

.card {
  --flip-height: 17.5em;
  
  animation-duration: 0.75s;
  animation-fill-mode: both;
  animation-timing-function: linear;
  /* NOTE: We're NOT setting the animation with CSS */
  /* By using Javascript, it's easier to prevent the animation from playing as soon as the page loads. */
}

/* ******************************************
 * END LEVEL 1 CODE
 * ******************************************/

/**
 * BASE CARD CSS
 */
.card {
  width: 100%;
  max-width: 15em;
  aspect-ratio: 5 / 7;
  border-radius: 0.75em;
  position: relative;
  transform-style: preserve-3d;
}

.card section {
  width: 100%;
  height: 100%;
  border-radius: inherit;
}

.card .face { backface-visibility: hidden; }

.card .back {
  position: absolute;
  inset: 0;
  transform: rotateY(180deg);
}

.perspective-container {
  perspective: 100em;
  perspective-origin: center;
}

/**
 * Base demo css. Not important for the demo; just to look pretty.
 */
:root {
  --blue: oklch(51% 0.129 259);
  --white: oklch(100% 0 0);
  --yellow: oklch(86% 0.18 89);
  --grey: oklch(32% 0 0);
  --electric: oklch(66% 0.137 91);
}

*, *::before, *::after { box-sizing: border-box; }

body {
  font-family:  -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
  background: radial-gradient(circle at 5vw 70vh, hsl(321 86% 91% / 0.5), hsl(321 86% 91% / 0) 67%),
        radial-gradient(circle at 50vw 5vh, hsl(120 50% 75% / 0.25), hsl(120 50% 75% / 0) 67%),
        radial-gradient(circle at 80vw 95vh, hsl(45 80% 65% / 0.25), hsl(45 50% 65% / 0) 67%);
  background-size: 100vw 100vh;
  background-size: 100dvw 100dvh;
  background-repeat: no-repeat;
}

.card p { margin: 0; }

.card section {
  border: 0.5em solid var(--blue);
  box-shadow: 0 0 0.15em 0.1em oklch(0% 0 0 / 0.75) inset;
  background: radial-gradient(var(--white) 25%, var(--yellow));
}

.front figure {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  margin: 0;
  padding-block: 1em;
}

.front figcaption {
  font-size: 1.333em;
  flex: 1;
  display: flex;
  align-items: center;
}

.front img {
  flex: 6;
  display: block;
  width: 80%;
  margin: 0 auto;
  object-fit: contain;
}

.back { padding: 1em 0.5em; }

.back header {
  display: flex;
  align-items: center;
}

.back header > :first-child {
  flex: 1;
}

.type-tag {
  color: white;
  font-size: 85%;
  font-weight: bold;
  text-align: center;
  line-height: 1;
  padding: 0.25em 1em;
  border-radius: 1em;
}

.type-tag.electric { background-color: var(--electric); }

.back dl {
  display: grid;
  grid-template-columns: 5.5ch 4ch 1fr;
}

.back dt { font-weight: bold; }
.back dt + dd { text-align: end; }
.back dd { margin: 0; }
.back dd + dd { padding-inline: 0.5em; }
.back [role="meter"] {
  height: 90%;
  width: calc(100% * (var(--value) / 255));
  border: 0.0625em solid;
}

.back footer {
  font-size: 0.875em;
  line-height: 1.25;
}

[role="meter"].hp {
  background-color: oklch(63% 0.258 29);
  border-color: oklch(46% 0.187 29);
}
[role="meter"].atk {
  background-color: oklch(71% 0.163 52);
  border-color: oklch(46% 0.187 29);
}
[role="meter"].def {
  background-color: oklch(87% 0.167 94);
  border-color: oklch(63% 0.119 94);
}
[role="meter"].spatk {
  background-color: oklch(67% 0.15 265);
  border-color: oklch(49% 0.105 266);
}
[role="meter"].spdef {
  background-color: oklch(76% 0.174 137);
  border-color: oklch(55% 0.123 137);
}
[role="meter"].spd {
  background-color: oklch(69% 0.198 6);
  border-color: oklch(50% 0.14 5);
}

.flip-demo {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.card { margin-block-end: 1em; }

button {
  font-size: 1.25em;
  border: none;
  color: var(--white);
  background-color: var(--grey);
  padding: 0.75em 1.25em;
  border-radius: 1.75em;
  box-shadow: 0 0.1em 0.5em oklch(0% 0 0 / 0.333);
  cursor: pointer;
}

button:hover,
button:focus {
  color: var(--grey);
  background-color: var(--white);
}

button:active { box-shadow: none; }
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))
}

function setupContent() {
  const front = demo.querySelector(".front")
  const back = demo.querySelector(".back")
  
  front.innerHTML = `
    <figure>
      <figcaption><strong>Pichu #172</strong></figcaption>
      <img src="https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/172.png" alt="Tiny yellow mouselike creature" />
    </figure>
  `
  
  back.innerHTML = `
    <header>
      <p><strong>Pichu</strong></p>
      <p class="type-tag electric">Electric</p>
    </header>
    <dl>
      <dt>HP</dt>
      <dd>20</dd>
      <dd><div class="hp" role="meter" aria-valuenow="20" aria-valuemin="0" aria-valuemax="255" aria-label="HP" style="--value: 20;"></div></dd>
      <dt>Atk</dt>
      <dd>40</dd>
      <dd><div class="atk" role="meter" aria-valuenow="40" aria-valuemin="0" aria-valuemax="255" aria-label="Attack" style="--value: 40;"></div></dd>
      <dt>Def</dt>
      <dd>15</dd>
      <dd><div class="def" role="meter" aria-valuenow="15" aria-valuemin="0" aria-valuemax="255" aria-label="Defense" style="--value: 15;"></div></dd>
      <dt>Sp.Atk</dt>
      <dd>35</dd>
      <dd><div class="spatk" role="meter" aria-valuenow="35" aria-valuemin="0" aria-valuemax="255" aria-label="Special Attack" style="--value: 35;"></div></dd>
      <dt>Sp.Def</dt>
      <dd>35</dd>
      <dd><div class="spdef" role="meter" aria-valuenow="35" aria-valuemin="0" aria-valuemax="255" aria-label="Special Defense" style="--value: 35;"></div></dd>
      <dt>Spd</dt>
      <dd>60</dd>
      <dd><div class="spd" role="meter" aria-valuenow="60" aria-valuemin="0" aria-valuemax="255" aria-label="Speed" style="--value: 60;"></div></dd>
    </dl>
    <footer>
      <p>The Tiny Mouse Pokémon. It still can’t use electricity well. When it’s surprised or excited, it discharges electricity unintentionally.</p>
    </footer>
  `
}

setupContent()
setupButton()

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.