CSS Flip Cards: 16 Examples + Perspective Values That Work

Most CSS flip cards turn on hover and stop there. That leaves touch screens and keyboards with no way to reach the back face.

Below are 16 CSS flip cards you can try and copy, followed by a guide to perspective values, triggers, accessibility and the bugs that flatten the turn.

By Bogdan Sandu · Updated October 28, 2026

Flip card mechanics

The building blocks: what triggers the turn, which axis it turns on, how much perspective it gets, and what happens when a visitor asks for less motion. Every card that hides content behind a button marks the turned-away face inert, so Tab and screen readers only reach the side that is showing.

Hover and focus flip

  • :focus-within
  • (hover: hover) and (pointer: fine)
  • backface-visibility
Old brick and white houses along a quiet canal, with two small boats moored at a wooden jetty

Canal quarter walk

2 hours · 4.2 km · Easy

What you will see

  • Three 17th century almshouses
  • The old fish market arcade
  • A lace workshop with a live demo
  • Coffee stop at the lock keeper house

From €18 per person

Hover the card, or press Tab to focus it.

The four-property recipe with two triggers. Hover only counts on devices with a fine pointer that can really hover, and :focus-within flips the card when it receives keyboard focus or a tap. A hover-only flip fails on touch screens and keyboards, so keep a CSS-only trigger like this for optional detail and use a button when the back holds anything a visitor needs.

View code
.fc-hover {
  width: min(280px, 100%);
  aspect-ratio: 4 / 5;
  border-radius: 18px;
  perspective: 1000px;
}
.fc-hover:focus-visible {
  outline: 3px solid #0b57d0;
  outline-offset: 5px;
}
.fc-hover__inner {
  position: relative;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
}
.fc-hover:focus-within .fc-hover__inner {
  transform: rotateY(180deg);
}
@media (hover: hover) and (pointer: fine) {
  .fc-hover:hover .fc-hover__inner {
    transform: rotateY(180deg);
  }
}
.fc-hover__face {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  border-radius: 18px;
  overflow: hidden;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  box-shadow: 0 18px 36px rgba(20, 32, 40, .18);
}
.fc-hover__face p {
  margin: 0;
}
.fc-hover__face--front {
  background: #fffaf2;
  color: #1d2a30;
}
.fc-hover__img {
  display: block;
  width: 100%;
  height: 68%;
  object-fit: cover;
}
.fc-hover__body {
  padding: 16px 18px;
}
.fc-hover__title {
  font: 600 22px/1.2 "Fraunces", Georgia, serif;
}
.fc-hover__meta {
  margin-top: 6px !important;
  color: #4a5a61;
  font-size: 14px;
}
.fc-hover__face--back {
  justify-content: center;
  padding: 26px 24px;
  background: #12343b;
  color: #f3efe6;
  transform: rotateY(180deg);
}
.fc-hover__list {
  display: grid;
  gap: 10px;
  margin: 16px 0 18px;
  padding: 0 0 0 18px;
  font-size: 15px;
  line-height: 1.4;
}
.fc-hover__list li::marker {
  color: #f0b44c;
}
.fc-hover__price {
  padding-top: 14px;
  border-top: 1px solid rgba(243, 239, 230, .25);
  color: #f0b44c;
  font-weight: 700;
}
@media (prefers-reduced-motion: no-preference) {
  .fc-hover__inner {
    transition: transform .7s cubic-bezier(.4, .2, .2, 1);
  }
}

Button-triggered flip

  • inert
  • focus()
  • rotateY(180deg)

Weeknight bake

Charred leek and gruyere tart

  • 45 min
  • Serves 4
  • Vegetarian

Leeks blistered in a hot pan, folded into a mustard custard and baked in ready-rolled puff pastry.

Ingredients

  • Leeks, sliced3
  • Puff pastry320 g
  • Gruyere, grated120 g
  • Eggs2
  • Creme fraiche150 ml
  • Dijon mustard1 tbsp

A real button on each face does the flipping, so mouse, touch and keyboard all work. The face that turns away gets the inert attribute, which removes it from the tab order and the accessibility tree, and focus moves to the button on the face that just came round. The button text says where it goes instead of using aria-pressed, which screen reader testers found confusing on flip cards.

View code
.fc-recipe {
  width: min(300px, 100%);
  perspective: 1100px;
}
.fc-recipe__card {
  position: relative;
  height: 360px;
  transform-style: preserve-3d;
}
.fc-recipe__card.is-flipped {
  transform: rotateY(180deg);
}
.fc-recipe__face {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  padding: 24px 22px 22px;
  border-radius: 6px;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  box-shadow: 0 2px 0 rgba(0, 0, 0, .06), 0 18px 34px rgba(60, 30, 10, .16);
}
.fc-recipe__face p {
  margin: 0;
}
.fc-recipe__face--front {
  background: #fdf6e9;
  color: #2b1d14;
  border-top: 8px solid #b3361f;
}
.fc-recipe__face--back {
  background: #b3361f;
  color: #fff7ec;
  transform: rotateY(180deg);
}
.fc-recipe__kicker {
  color: #8c2a17;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
}
.fc-recipe__title {
  margin-top: 6px !important;
  font: 600 25px/1.15 "Fraunces", Georgia, serif;
}
.fc-recipe__chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 14px 0 0;
  padding: 0;
  list-style: none;
}
.fc-recipe__chips li {
  padding: 4px 10px;
  border: 1px solid #d9c6ad;
  border-radius: 99px;
  font-size: 13px;
}
.fc-recipe__text {
  margin-top: 14px !important;
  color: #4d3a2c;
  font-size: 15px;
  line-height: 1.5;
}
.fc-recipe__list {
  margin: 14px 0 0;
  padding: 0;
  list-style: none;
  font-size: 15px;
}
.fc-recipe__list li {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 7px 0;
  border-bottom: 1px dashed rgba(255, 247, 236, .4);
}
.fc-recipe__list li span:last-child {
  font-variant-numeric: tabular-nums;
  font-weight: 700;
}
.fc-recipe__btn {
  min-height: 44px;
  margin-top: auto;
  padding: 0 18px;
  border: 0;
  border-radius: 6px;
  background: #b3361f;
  color: #fff;
  font: inherit;
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
}
.fc-recipe__face--back .fc-recipe__btn {
  background: #fdf6e9;
  color: #8c2a17;
}
.fc-recipe__btn:focus-visible {
  outline: 3px solid #1d3fbf;
  outline-offset: 3px;
}
.fc-recipe__face--back .fc-recipe__btn:focus-visible {
  outline-color: #fff;
}
@media (prefers-reduced-motion: no-preference) {
  .fc-recipe__card {
    transition: transform .65s cubic-bezier(.3, .7, .2, 1);
  }
}

Vertical flip on rotateX

  • rotateX(180deg)
  • hard-stop barcode
  • inert

Northline Festival

Saturday 13 March 2027 · Gates 14:00

Gate
C
Area
Floor
Type
Day pass

0491 2286 7713

One entry per scan. No re-entry after 21:00.

The same recipe turned 90 degrees: the card and the back face both use rotateX(180deg), so the ticket tumbles top over bottom like a pass being turned in your hand. Keep the pre-rotation of the back face on the same axis as the trigger rotation, or the back text ends up upside down.

View code
.fc-ticket {
  width: min(330px, 100%);
  perspective: 900px;
}
.fc-ticket__card {
  position: relative;
  height: 250px;
  transform-style: preserve-3d;
}
.fc-ticket__card.is-flipped {
  transform: rotateX(180deg);
}
.fc-ticket__face {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  padding: 20px 22px;
  border-radius: 14px;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
}
.fc-ticket__face p,
.fc-ticket__face dl,
.fc-ticket__face dd {
  margin: 0;
}
.fc-ticket__face--front {
  background: #f4c430;
  color: #16130b;
}
.fc-ticket__face--back {
  align-items: center;
  background: #fbf8ef;
  color: #16130b;
  transform: rotateX(180deg);
}
.fc-ticket__main {
  padding-bottom: 14px;
  border-bottom: 2px dashed rgba(22, 19, 11, .45);
}
.fc-ticket__event {
  font: 800 27px/1.05 "Fraunces", Georgia, serif;
  letter-spacing: -.01em;
}
.fc-ticket__date {
  margin-top: 6px !important;
  font-size: 14px;
  font-weight: 600;
}
.fc-ticket__grid {
  display: flex;
  gap: 22px;
  margin-top: 14px !important;
}
.fc-ticket__grid dt {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
}
.fc-ticket__grid dd {
  font-size: 18px;
  font-weight: 700;
}
.fc-ticket__code {
  width: 100%;
  height: 74px;
  background: repeating-linear-gradient(90deg, #16130b 0 3px, transparent 3px 5px, #16130b 5px 6px, transparent 6px 9px, #16130b 9px 13px, transparent 13px 15px);
}
.fc-ticket__num {
  margin-top: 8px !important;
  font: 600 15px/1 ui-monospace, Consolas, monospace;
  letter-spacing: .12em;
}
.fc-ticket__rules {
  margin-top: 10px !important;
  font-size: 13px;
  text-align: center;
}
.fc-ticket__btn {
  align-self: stretch;
  min-height: 44px;
  margin-top: auto;
  border: 0;
  border-radius: 10px;
  background: #16130b;
  color: #f4c430;
  font: inherit;
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
}
.fc-ticket__btn:focus-visible {
  outline: 3px solid #fff;
  outline-offset: 3px;
}
@media (prefers-reduced-motion: no-preference) {
  .fc-ticket__card {
    transition: transform .7s cubic-bezier(.45, .1, .2, 1);
  }
}

Perspective 400, 800 and 1200px

  • perspective
  • range input
  • custom property
Front
Back

400px

Front
Back

800px

Front
Back

1200px

58deg

Drag the slider to turn all three cards.

Three identical cards on three containers with different perspective values, all driven by one slider. A small perspective is a viewer standing close: the near edge balloons and the far edge shrinks. A large one flattens the turn. Around three to five times the card width is a comfortable middle.

View code
.fc-persp {
  width: min(520px, 100%);
}
.fc-persp__row {
  display: flex;
  justify-content: center;
  gap: 14px;
}
.fc-persp__col {
  flex: 1 1 0;
  max-width: 150px;
  min-width: 0;
}
.fc-persp__view {
  padding: 10px 0;
}
.fc-persp__card {
  position: relative;
  aspect-ratio: 3 / 4;
  transform: rotateY(calc(var(--fc-angle, 58) * 1deg));
  transform-style: preserve-3d;
}
.fc-persp__face {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  border-radius: 10px;
  background: #2340d6;
  color: #fff;
  font-size: 15px;
  font-weight: 700;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  box-shadow: inset 0 0 0 6px #4c66f0;
}
.fc-persp__face--back {
  background: #ff7a45;
  color: #1a0f08;
  box-shadow: inset 0 0 0 6px #ffa47d;
  transform: rotateY(180deg);
}
.fc-persp__label {
  margin: 8px 0 0;
  text-align: center;
}
.fc-persp__label code {
  padding: 2px 6px;
  border-radius: 4px;
  background: #e3e6ef;
  color: #1b2030;
  font: 600 13px/1.4 ui-monospace, Consolas, monospace;
}
.fc-persp__control {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 20px;
  padding: 10px 14px;
  border-radius: 12px;
  background: #fff;
  color: #1b2030;
  box-shadow: 0 6px 18px rgba(20, 30, 60, .08);
}
.fc-persp__control label {
  font: 600 14px/1 ui-monospace, Consolas, monospace;
}
.fc-persp__control input {
  flex: 1;
  min-width: 0;
  height: 28px;
  accent-color: #2340d6;
}
.fc-persp__control input:focus-visible {
  outline: 3px solid #2340d6;
  outline-offset: 2px;
}
.fc-persp__control output {
  min-width: 52px;
  font: 600 14px/1 ui-monospace, Consolas, monospace;
  text-align: right;
}

Flip with real thickness

  • translateZ
  • edge planes
  • preserve-3d

Fernhill Public Library

Marta Lindqvist

Member 2207 5531

Borrowing

Up to 12 items for 3 weeks. Return to any of the six branches or the 24-hour drop box.

Lost card? Call 0161 555 0142.

Real cards have an edge. The two faces sit 5px in front of and behind the center plane with translateZ, and four thin edge planes are rotated 90 degrees to close the gap, so the card shows a solid rim halfway through the turn. The toggle button lives outside the card, so focus never has to move.

View code
.fc-depth {
  display: grid;
  justify-items: center;
  gap: 22px;
  width: min(300px, 100%);
}
.fc-depth__view {
  width: 100%;
  perspective: 900px;
}
.fc-depth__card {
  position: relative;
  aspect-ratio: 85.6 / 54;
  transform: rotateX(10deg) rotateY(-24deg);
  transform-style: preserve-3d;
}
.fc-depth__card.is-flipped {
  transform: rotateX(10deg) rotateY(156deg);
}
.fc-depth__face {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  padding: 16px 18px;
  border-radius: 4px;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
}
.fc-depth__face p {
  margin: 0;
}
.fc-depth__face--front {
  background: #1f6f5c;
  color: #f2f7f4;
  transform: translateZ(5px);
}
.fc-depth__face--back {
  background: #f2efe6;
  color: #1c2622;
  transform: rotateY(180deg) translateZ(5px);
}
.fc-depth__edge {
  position: absolute;
  background: #d8cfb4;
}
.fc-depth__edge--l,
.fc-depth__edge--r {
  top: 0;
  width: 10px;
  height: 100%;
}
.fc-depth__edge--l {
  left: 0;
  transform: translateX(-5px) rotateY(-90deg);
}
.fc-depth__edge--r {
  right: 0;
  transform: translateX(5px) rotateY(90deg);
}
.fc-depth__edge--t,
.fc-depth__edge--b {
  left: 0;
  width: 100%;
  height: 10px;
}
.fc-depth__edge--t {
  top: 0;
  transform: translateY(-5px) rotateX(90deg);
}
.fc-depth__edge--b {
  bottom: 0;
  transform: translateY(5px) rotateX(-90deg);
}
.fc-depth__org {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
}
.fc-depth__name {
  margin-top: auto !important;
  font: 600 21px/1.2 "Fraunces", Georgia, serif;
}
.fc-depth__id {
  margin-top: 4px !important;
  font: 500 14px/1.3 ui-monospace, Consolas, monospace;
  letter-spacing: .06em;
}
.fc-depth__back-title {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
}
.fc-depth__back-text {
  margin-top: 6px !important;
  font-size: 13px;
  line-height: 1.4;
}
.fc-depth__btn {
  min-height: 44px;
  padding: 0 20px;
  border: 2px solid #1f6f5c;
  border-radius: 99px;
  background: #fff;
  color: #134537;
  font: inherit;
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
}
.fc-depth__btn:focus-visible {
  outline: 3px solid #134537;
  outline-offset: 3px;
}
@media (prefers-reduced-motion: no-preference) {
  .fc-depth__card {
    transition: transform .9s cubic-bezier(.35, .1, .15, 1);
  }
}

Reduced motion crossfade

  • prefers-reduced-motion
  • opacity crossfade
  • aria-pressed

4 minute exercise

Box breathing

A slow, even pattern that helps you settle before a presentation or a hard conversation.

  1. Breathe in for 4 counts
  2. Hold for 4 counts
  3. Breathe out for 4 counts
  4. Hold for 4 counts, then repeat

With no motion preference the card rotates. With prefers-reduced-motion: reduce it drops the 3D transform entirely and crossfades the two faces in place, which still tells the visitor that the content changed. The preview toggle applies the same rules without touching your system settings.

View code
.fc-calm {
  display: grid;
  justify-items: center;
  gap: 18px;
  width: min(300px, 100%);
}
.fc-calm__view {
  width: 100%;
  perspective: 1000px;
}
.fc-calm__card {
  position: relative;
  height: 320px;
  transform-style: preserve-3d;
}
.fc-calm__card.is-flipped {
  transform: rotateY(180deg);
}
.fc-calm__face {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  padding: 26px 24px 22px;
  border-radius: 28px;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  transition: opacity .45s ease;
}
.fc-calm__face p {
  margin: 0;
}
.fc-calm__face--front {
  background: #dfe9e4;
  color: #183029;
}
.fc-calm__face--back {
  background: #36574d;
  color: #f1f6f3;
  transform: rotateY(180deg);
}
.fc-calm__tag {
  align-self: flex-start;
  padding: 4px 10px;
  border-radius: 99px;
  background: #fff;
  font-size: 13px;
  font-weight: 600;
}
.fc-calm__title {
  margin-top: 18px !important;
  font: 500 30px/1.1 "Fraunces", Georgia, serif;
}
.fc-calm__text {
  margin-top: 10px !important;
  font-size: 15px;
  line-height: 1.5;
}
.fc-calm__steps {
  display: grid;
  gap: 12px;
  margin: 0;
  padding: 0 0 0 22px;
  font-size: 16px;
  line-height: 1.35;
}
.fc-calm__btn {
  min-height: 44px;
  margin-top: auto;
  border: 0;
  border-radius: 99px;
  background: #183029;
  color: #fff;
  font: inherit;
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
}
.fc-calm__face--back .fc-calm__btn {
  background: #f1f6f3;
  color: #183029;
}
.fc-calm__toggle {
  min-height: 36px;
  padding: 0 14px 0 40px;
  border: 1px solid #b9c7c0;
  border-radius: 99px;
  background: #fff;
  color: #183029;
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  position: relative;
}
.fc-calm__toggle::before {
  content: "";
  position: absolute;
  left: 8px;
  top: 50%;
  width: 22px;
  height: 22px;
  margin-top: -11px;
  border-radius: 50%;
  border: 2px solid #36574d;
  box-sizing: border-box;
}
.fc-calm__toggle[aria-pressed="true"]::before {
  background: #36574d;
  box-shadow: inset 0 0 0 3px #fff;
}
.fc-calm__toggle:disabled {
  cursor: default;
}
.fc-calm__btn:focus-visible,
.fc-calm__toggle:focus-visible {
  outline: 3px solid #1d3fbf;
  outline-offset: 3px;
}
.fc-calm.is-calm .fc-calm__card,
.fc-calm.is-calm .fc-calm__face--back {
  transform: none;
}
.fc-calm.is-calm .fc-calm__face--back,
.fc-calm.is-calm .fc-calm__card.is-flipped .fc-calm__face--front {
  opacity: 0;
}
.fc-calm.is-calm .fc-calm__card.is-flipped .fc-calm__face--back {
  opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
  .fc-calm__card,
  .fc-calm__face--back {
    transform: none;
  }
  .fc-calm__card.is-flipped {
    transform: none;
  }
  .fc-calm__face--back,
  .fc-calm__card.is-flipped .fc-calm__face--front {
    opacity: 0;
  }
  .fc-calm__card.is-flipped .fc-calm__face--back {
    opacity: 1;
  }
}
@media (prefers-reduced-motion: no-preference) {
  .fc-calm:not(.is-calm) .fc-calm__card {
    transition: transform .7s cubic-bezier(.3, .7, .2, 1);
  }
}

Flip cards for real interfaces

Team bios, product specs, pricing, business cards, playing cards and a dark weather widget. Each one keeps the essential information on the front and uses the back for detail, which is the case where a flip card earns its place.

Team member card

  • inert
  • focus()
  • mailto link

Tomas Okafor

Lead backend engineer

About Tomas

Runs the payments platform team. Before joining, he spent six years building logistics APIs and still keeps a spreadsheet of every outage he has fixed.

  • Go
  • PostgreSQL
  • Kafka
  • On-call lead
tomas@example.com

The classic use: photo or avatar, name and role on the front, bio, skills and a contact link on the back. Because the back holds a link, it has to be reachable by keyboard, so the flip runs from a button and the back stays inert until it turns round.

View code
.fc-team {
  width: min(280px, 100%);
  perspective: 1000px;
}
.fc-team__card {
  position: relative;
  height: 390px;
  transform-style: preserve-3d;
}
.fc-team__card.is-flipped {
  transform: rotateY(-180deg);
}
.fc-team__face {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  padding: 18px 18px 20px;
  border-radius: 22px;
  background: #fff;
  color: #1a1f36;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  box-shadow: 0 20px 40px rgba(26, 31, 54, .14);
}
.fc-team__face p {
  margin: 0;
}
.fc-team__face--back {
  padding: 24px 22px 20px;
  background: #1a1f36;
  color: #eef0fa;
  transform: rotateY(180deg);
}
.fc-team__art {
  position: relative;
  display: grid;
  place-items: center;
  height: 170px;
  margin-bottom: 16px;
  border-radius: 14px;
  background: #ffcf5c;
  overflow: hidden;
}
.fc-team__art::before,
.fc-team__art::after {
  content: "";
  position: absolute;
  border-radius: 50%;
}
.fc-team__art::before {
  width: 150px;
  height: 150px;
  right: -40px;
  bottom: -60px;
  background: #ff8a5c;
}
.fc-team__art::after {
  width: 60px;
  height: 60px;
  left: 18px;
  top: 16px;
  border: 10px solid #1a1f36;
  box-sizing: border-box;
}
.fc-team__art span {
  position: relative;
  z-index: 1;
  display: grid;
  place-items: center;
  width: 92px;
  height: 92px;
  border-radius: 50%;
  background: #1a1f36;
  color: #ffcf5c;
  font: 600 32px/1 "Fraunces", Georgia, serif;
}
.fc-team__name {
  font: 600 23px/1.2 "Fraunces", Georgia, serif;
}
.fc-team__role {
  margin-top: 4px !important;
  color: #4b5170;
  font-size: 15px;
}
.fc-team__bio {
  margin-top: 10px !important;
  font-size: 14px;
  line-height: 1.5;
}
.fc-team__skills {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 14px 0 0;
  padding: 0;
  list-style: none;
}
.fc-team__skills li {
  padding: 4px 10px;
  border-radius: 6px;
  background: #2c3354;
  font-size: 13px;
}
.fc-team__link {
  align-self: flex-start;
  margin-top: 14px;
  padding: 4px 0;
  color: #ffcf5c;
  font-size: 15px;
  font-weight: 600;
  text-underline-offset: 3px;
}
.fc-team__btn {
  min-height: 44px;
  margin-top: auto;
  border: 2px solid #1a1f36;
  border-radius: 12px;
  background: #fff;
  color: #1a1f36;
  font: inherit;
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
}
.fc-team__face--back .fc-team__btn {
  border-color: #eef0fa;
  background: #eef0fa;
}
.fc-team__btn:focus-visible,
.fc-team__link:focus-visible {
  outline: 3px solid #5468ff;
  outline-offset: 3px;
}
@media (prefers-reduced-motion: no-preference) {
  .fc-team__card {
    transition: transform .7s cubic-bezier(.3, .7, .2, 1);
  }
}

Product card with specs on the back

  • description list
  • inert
  • object-fit
Illustration of a dark green desk lamp with an angled arm and a warm yellow light

Arc desk lamp

€129

Forest green, powder-coated steel

Specifications

Height
46 cm, arm extended
Bulb
E27 LED, 8 W, 2700 K
Cable
2 m textile, inline switch
Weight
1.6 kg
Warranty
3 years

Shop cards get crowded fast. The front keeps the image, name, price and finish, and the back carries the spec sheet as a description list, so screen readers announce each term with its value. The add-to-cart button stays on the front, where it belongs.

View code
.fc-product {
  width: min(290px, 100%);
  perspective: 1100px;
}
.fc-product__card {
  position: relative;
  height: 400px;
  transform-style: preserve-3d;
}
.fc-product__card.is-flipped {
  transform: rotateY(180deg);
}
.fc-product__face {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  padding: 12px 12px 16px;
  border-radius: 16px;
  background: #fff;
  color: #20262b;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  box-shadow: 0 1px 2px rgba(0, 0, 0, .08), 0 14px 30px rgba(40, 40, 30, .12);
}
.fc-product__face p {
  margin: 0;
}
.fc-product__face--back {
  padding: 22px 20px 16px;
  background: #2c3a34;
  color: #f4f1ea;
  transform: rotateY(180deg);
}
.fc-product__img {
  display: block;
  width: 100%;
  height: 200px;
  border-radius: 10px;
  object-fit: cover;
}
.fc-product__row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 10px;
  margin-top: 14px;
  padding: 0 4px;
}
.fc-product__name {
  font-size: 19px;
  font-weight: 700;
}
.fc-product__price {
  font-size: 19px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.fc-product__finish {
  padding: 4px 4px 0;
  color: #57615d;
  font-size: 14px;
}
.fc-product__actions {
  display: flex;
  gap: 8px;
  margin-top: auto;
}
.fc-product__btn {
  min-height: 44px;
  padding: 0 16px;
  border: 1px solid #c9cec8;
  border-radius: 10px;
  background: #fff;
  color: #20262b;
  font: inherit;
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
}
.fc-product__btn--main {
  flex: 1;
  border-color: #c8693c;
  background: #c8693c;
  color: #1c0e06;
}
.fc-product__face--back .fc-product__btn {
  margin-top: auto;
  border-color: #f4f1ea;
  background: #f4f1ea;
  color: #2c3a34;
}
.fc-product__btn:focus-visible {
  outline: 3px solid #1d3fbf;
  outline-offset: 2px;
}
.fc-product__face--back .fc-product__btn:focus-visible {
  outline-color: #f2c45a;
}
.fc-product__specs {
  margin: 12px 0 0;
}
.fc-product__specs div {
  display: grid;
  grid-template-columns: 80px 1fr;
  gap: 10px;
  padding: 9px 0;
  border-bottom: 1px solid rgba(244, 241, 234, .18);
  font-size: 14px;
}
.fc-product__specs dt {
  color: #f2c45a;
  font-weight: 700;
}
.fc-product__specs dd {
  margin: 0;
}
@media (prefers-reduced-motion: no-preference) {
  .fc-product__card {
    transition: transform .6s cubic-bezier(.3, .7, .2, 1);
  }
}

Pricing card, monthly or yearly

  • aria-pressed switch
  • inert
  • tabular-nums

Studio

€24 per month

  • 5 editors, unlimited viewers
  • Version history for 90 days
  • Custom domain and SSO

Studio Save €58

€230 per year

Works out at €19.17 a month

  • Everything in monthly
  • Priority support

Here the trigger is a billing switch above the card. Billing really is an on or off setting, so aria-pressed fits the switch, and the card turns to show the yearly price. Each face has its own trial button, and the hidden face is inert so its button never lands in the tab order twice.

View code
.fc-price {
  display: grid;
  justify-items: center;
  gap: 18px;
  width: min(300px, 100%);
}
.fc-price__bar {
  display: flex;
  align-items: center;
  gap: 10px;
  color: #14161f;
  font-size: 14px;
  font-weight: 600;
}
.fc-price__lbl b {
  color: #3d6b00;
}
.fc-price__switch {
  position: relative;
  width: 52px;
  height: 30px;
  padding: 0;
  border: 0;
  border-radius: 99px;
  background: #14161f;
  cursor: pointer;
}
.fc-price__switch::after {
  content: "";
  position: absolute;
  top: 4px;
  left: 4px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: #c6f36b;
}
.fc-price__switch[aria-pressed="true"]::after {
  transform: translateX(22px);
}
.fc-price__switch:focus-visible {
  outline: 3px solid #5a3ff5;
  outline-offset: 3px;
}
.fc-price__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.fc-price__view {
  width: 100%;
  perspective: 1200px;
}
.fc-price__card {
  position: relative;
  height: 330px;
  transform-style: preserve-3d;
}
.fc-price__card.is-flipped {
  transform: rotateY(180deg);
}
.fc-price__face {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  padding: 24px 22px 20px;
  border-radius: 20px;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
}
.fc-price__face p {
  margin: 0;
}
.fc-price__face--front {
  border: 2px solid #14161f;
  background: #fff;
  color: #14161f;
}
.fc-price__face--back {
  background: #14161f;
  color: #f5f6fa;
  transform: rotateY(180deg);
}
.fc-price__plan {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 15px;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
}
.fc-price__save {
  padding: 3px 8px;
  border-radius: 6px;
  background: #c6f36b;
  color: #14161f;
  font-size: 12px;
  letter-spacing: 0;
  text-transform: none;
}
.fc-price__amount {
  margin-top: 10px !important;
  font-size: 15px;
}
.fc-price__amount span {
  font: 600 46px/1 "Fraunces", Georgia, serif;
  font-variant-numeric: tabular-nums;
  letter-spacing: -.02em;
}
.fc-price__per {
  margin-top: 4px !important;
  color: #c6f36b;
  font-size: 14px;
}
.fc-price__list {
  display: grid;
  gap: 8px;
  margin: 16px 0 0;
  padding: 0 0 0 18px;
  font-size: 14px;
}
.fc-price__cta {
  min-height: 44px;
  margin-top: auto;
  border: 0;
  border-radius: 12px;
  background: #14161f;
  color: #fff;
  font: inherit;
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
}
.fc-price__face--back .fc-price__cta {
  background: #c6f36b;
  color: #14161f;
}
.fc-price__cta:focus-visible {
  outline: 3px solid #5a3ff5;
  outline-offset: 3px;
}
@media (prefers-reduced-motion: no-preference) {
  .fc-price__card {
    transition: transform .7s cubic-bezier(.3, .7, .2, 1);
  }
  .fc-price__switch::after {
    transition: transform .2s ease;
  }
}

Business card

  • aspect-ratio 85 / 55
  • icon button
  • tel: and mailto:

Halden & Vos

Architects · Rotterdam

Saskia Vos

Partner, adaptive reuse

A business card at the standard 85 by 55 mm ratio, kept proportional with aspect-ratio. Each face carries a small labelled turn button in its corner, the back holds working phone and email links, and the back face stays inert until it is showing.

View code
.fc-biz {
  width: min(340px, 100%);
  perspective: 1000px;
}
.fc-biz__card {
  position: relative;
  aspect-ratio: 85 / 55;
  transform-style: preserve-3d;
}
.fc-biz__card.is-flipped {
  transform: rotateY(180deg);
}
.fc-biz__face {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  padding: 20px 22px;
  border-radius: 6px;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  box-shadow: 0 22px 40px rgba(0, 0, 0, .45);
}
.fc-biz__face p {
  margin: 0;
}
.fc-biz__face--front {
  justify-content: flex-end;
  background: #f1ece2;
  color: #1d2b4f;
}
.fc-biz__face--back {
  justify-content: center;
  background: #1d2b4f;
  color: #f1ece2;
  transform: rotateY(180deg);
}
.fc-biz__mark {
  position: absolute;
  top: 20px;
  left: 22px;
  width: 34px;
  height: 34px;
  background: #1d2b4f;
}
.fc-biz__mark::after {
  content: "";
  position: absolute;
  right: -12px;
  bottom: -12px;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: #d0573a;
  mix-blend-mode: multiply;
}
.fc-biz__firm {
  font: 600 28px/1.05 "Fraunces", Georgia, serif;
  letter-spacing: -.01em;
}
.fc-biz__tag {
  margin-top: 4px !important;
  color: #4a5370;
  font-size: 13px;
  letter-spacing: .06em;
  text-transform: uppercase;
}
.fc-biz__name {
  font: 600 22px/1.1 "Fraunces", Georgia, serif;
}
.fc-biz__title {
  margin-top: 2px !important;
  color: #c8cde0;
  font-size: 13px;
}
.fc-biz__links {
  display: grid;
  gap: 2px;
  margin-top: 12px !important;
  font-size: 14px;
}
.fc-biz__links a {
  justify-self: start;
  padding: 3px 0;
  color: #f1ece2;
  text-underline-offset: 3px;
}
.fc-biz__turn {
  position: absolute;
  right: 10px;
  bottom: 10px;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: inherit;
  cursor: pointer;
}
.fc-biz__turn:hover {
  background: rgba(127, 127, 127, .18);
}
.fc-biz__turn:focus-visible,
.fc-biz__links a:focus-visible {
  outline: 3px solid #d0573a;
  outline-offset: 2px;
}
@media (prefers-reduced-motion: no-preference) {
  .fc-biz__card {
    transition: transform .8s cubic-bezier(.5, -.2, .3, 1.2);
  }
}

Playing cards, face down

  • card as button
  • aria-label swap
  • rotateY(-180deg)

Click a card to turn it over.

Three cards that start face down. Each whole card is a button, which works here because the faces hold no headings or links that a button role would flatten. The visual faces are hidden from assistive tech and the button label carries the state: Card 2, face down, then Card 2, queen of hearts.

View code
.fc-deal {
  display: flex;
  justify-content: center;
  gap: clamp(10px, 3vw, 22px);
  width: min(440px, 100%);
  box-sizing: border-box;
  padding: 34px clamp(12px, 4vw, 34px);
  border-radius: 24px;
  background: #1f5b3a;
  box-shadow: inset 0 0 0 10px #18492e;
}
.fc-deal__card {
  flex: 1 1 0;
  max-width: 110px;
  min-width: 0;
  aspect-ratio: 5 / 7;
  padding: 0;
  border: 0;
  border-radius: 9px;
  background: none;
  font: inherit;
  cursor: pointer;
  perspective: 700px;
}
.fc-deal__card:focus-visible {
  outline: 3px solid #ffd447;
  outline-offset: 4px;
}
.fc-deal__inner {
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
}
.fc-deal__card.is-up .fc-deal__inner {
  transform: rotateY(-180deg);
}
.fc-deal__face {
  position: absolute;
  inset: 0;
  border-radius: 9px;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  box-shadow: 0 8px 16px rgba(0, 0, 0, .3);
}
.fc-deal__face--down {
  background: #f7f2e7;
}
.fc-deal__face--down::before {
  content: "";
  position: absolute;
  inset: 6px;
  border-radius: 5px;
  background: #8e1f2f;
  box-shadow: inset 0 0 0 3px #8e1f2f, inset 0 0 0 5px #f7f2e7;
}
.fc-deal__face--down::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 34%;
  aspect-ratio: 1;
  border: 3px solid #f7f2e7;
  transform: translate(-50%, -50%) rotate(45deg);
}
.fc-deal__face--up {
  display: grid;
  place-items: center;
  background: #fff;
  color: #14171c;
  transform: rotateY(180deg);
}
.fc-deal__face--red {
  color: #b3172b;
}
.fc-deal__rank {
  position: absolute;
  top: 6px;
  left: 8px;
  font: 700 17px/1 "Fraunces", Georgia, serif;
  text-align: center;
}
.fc-deal__pip {
  font-size: clamp(34px, 9vw, 52px);
  line-height: 1;
}
@media (prefers-reduced-motion: no-preference) {
  .fc-deal__inner {
    transition: transform .55s cubic-bezier(.3, .7, .2, 1);
  }
  .fc-deal__card:hover .fc-deal__inner {
    translate: 0 -4px;
  }
}

Dark theme weather card

  • dark palette
  • inert
  • tabular-nums

Port Aldren

Tuesday, 15:00

14°

Light rain, clearing by evening

Wind 18 km/h SW · Humidity 82%

Next 5 days

  • Wed9°15°
  • Thu8°13°
  • Fri11°18°
  • Sat12°20°
  • Sun10°16°

A dark widget that keeps contrast in check on both faces: light text on charcoal, one amber accent and no glow. Today sits on the front, the five-day forecast on the back, and the bars on the back show each day range at a glance.

View code
.fc-wx {
  width: min(290px, 100%);
  perspective: 1000px;
}
.fc-wx__card {
  position: relative;
  height: 360px;
  transform-style: preserve-3d;
}
.fc-wx__card.is-flipped {
  transform: rotateY(180deg);
}
.fc-wx__face {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  padding: 22px 22px 20px;
  border: 1px solid #2c3242;
  border-radius: 22px;
  background: #171b25;
  color: #e8ebf2;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
}
.fc-wx__face p {
  margin: 0;
}
.fc-wx__face--back {
  background: #1d2230;
  transform: rotateY(180deg);
}
.fc-wx__city {
  font-size: 18px;
  font-weight: 700;
}
.fc-wx__day {
  margin-top: 2px !important;
  color: #a9b0c2;
  font-size: 14px;
}
.fc-wx__now {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 16px;
}
.fc-wx__icon {
  flex: none;
}
.fc-wx__temp {
  font: 300 64px/1 "Fraunces", Georgia, serif;
  letter-spacing: -.03em;
}
.fc-wx__cond {
  margin-top: 12px !important;
  font-size: 15px;
}
.fc-wx__meta {
  margin-top: 6px !important;
  color: #a9b0c2;
  font-size: 13px;
}
.fc-wx__days {
  display: grid;
  gap: 4px;
  margin: 14px 0 0;
  padding: 0;
  list-style: none;
}
.fc-wx__days li {
  display: grid;
  grid-template-columns: 38px 30px 1fr 30px;
  align-items: center;
  gap: 8px;
  padding: 7px 0;
  border-bottom: 1px solid #2c3242;
  font-size: 15px;
  font-variant-numeric: tabular-nums;
}
.fc-wx__lo {
  color: #a9b0c2;
  text-align: right;
}
.fc-wx__bar {
  position: relative;
  height: 6px;
  border-radius: 3px;
  background: #2c3242;
}
.fc-wx__bar::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--fc-a);
  right: calc(100% - var(--fc-b));
  border-radius: 3px;
  background: #ffb547;
}
.fc-wx__btn {
  min-height: 44px;
  margin-top: auto;
  border: 1px solid #3a4154;
  border-radius: 12px;
  background: #232937;
  color: #e8ebf2;
  font: inherit;
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
}
.fc-wx__btn:hover {
  border-color: #ffb547;
}
.fc-wx__btn:focus-visible {
  outline: 3px solid #ffb547;
  outline-offset: 3px;
}
@media (prefers-reduced-motion: no-preference) {
  .fc-wx__card {
    transition: transform .7s cubic-bezier(.3, .7, .2, 1);
  }
}

Decks, quizzes and grids

Flip cards that work as a set: a flashcard deck with progress, a quiz that turns over to grade the answer, a grid that flips in sequence, and a transit pass inside a phone screen.

Flashcard study deck

  • textContent
  • progress bar
  • aria-live

Card 1 of 6

Term

perspective

Definition

Sets the distance between the viewer and the z=0 plane, which gives the 3D children of an element their depth.

One card, six terms. Flip shows the definition, Previous and Next move through the deck and reset the card to the front without animating the reset, and a live region announces the position. The controls sit outside the card, so the inert face swap never steals focus.

View code
.fc-deck {
  display: grid;
  gap: 16px;
  width: min(360px, 100%);
}
.fc-deck p {
  margin: 0;
}
.fc-deck__top {
  display: flex;
  align-items: center;
  gap: 14px;
}
.fc-deck__count {
  flex: none;
  color: #2a2350;
  font-size: 14px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.fc-deck__track {
  flex: 1;
  height: 8px;
  border-radius: 4px;
  background: #ddd8f3;
  overflow: hidden;
}
.fc-deck__fill {
  display: block;
  width: 0;
  height: 100%;
  border-radius: 4px;
  background: #5b45d6;
}
.fc-deck__view {
  perspective: 1200px;
}
.fc-deck__card {
  position: relative;
  height: 230px;
  transform-style: preserve-3d;
}
.fc-deck__card.is-flipped {
  transform: rotateY(180deg);
}
.fc-deck__face {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  box-sizing: border-box;
  padding: 22px 24px;
  border-radius: 18px;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  box-shadow: 0 6px 0 #c9c1ef, 0 18px 30px rgba(42, 35, 80, .14);
}
.fc-deck__face--front {
  align-items: center;
  background: #fff;
  color: #2a2350;
  text-align: center;
}
.fc-deck__face--back {
  background: #2a2350;
  color: #f2f0fb;
  transform: rotateY(180deg);
}
.fc-deck__label {
  position: absolute;
  top: 16px;
  left: 20px;
  color: #6b6394;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
}
.fc-deck__face--back .fc-deck__label {
  color: #b9b1e6;
}
.fc-deck__term {
  font: 600 clamp(22px, 6vw, 28px)/1.2 ui-monospace, Consolas, monospace;
  overflow-wrap: anywhere;
}
.fc-deck__def {
  margin-top: 12px !important;
  font-size: 16px;
  line-height: 1.5;
}
.fc-deck__controls {
  display: grid;
  grid-template-columns: 1fr 1.3fr 1fr;
  gap: 8px;
}
.fc-deck__btn {
  min-height: 44px;
  padding: 0 8px;
  border: 1px solid #c9c1ef;
  border-radius: 12px;
  background: #fff;
  color: #2a2350;
  font: inherit;
  font-size: 14px;
  font-weight: 700;
  cursor: pointer;
}
.fc-deck__btn--flip {
  border-color: #5b45d6;
  background: #5b45d6;
  color: #fff;
}
.fc-deck__btn:focus-visible {
  outline: 3px solid #2a2350;
  outline-offset: 2px;
}
@media (prefers-reduced-motion: no-preference) {
  .fc-deck__card {
    transition: transform .6s cubic-bezier(.3, .7, .2, 1);
  }
  .fc-deck__card.is-instant {
    transition: none;
  }
  .fc-deck__fill {
    transition: width .3s ease;
  }
}

Quiz card with answer reveal

  • form submit
  • radio group
  • tabindex="-1" focus
Which declaration keeps both faces of a flip card in 3D space?

Correct

transform-style: preserve-3d on the rotating wrapper. Without it the faces are flattened into the wrapper plane and both show at once.

The front is a real form with a fieldset and radio buttons. Submitting it is cancelled in script, the card turns over, and focus moves to the result heading on the back so a screen reader reads the verdict first. Try again resets the form and returns focus to the first option.

View code
.fc-quiz {
  width: min(330px, 100%);
  perspective: 1100px;
}
.fc-quiz__card {
  position: relative;
  height: 360px;
  transform-style: preserve-3d;
}
.fc-quiz__card.is-flipped {
  transform: rotateY(180deg);
}
.fc-quiz__face {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  margin: 0;
  padding: 22px 20px 20px;
  border-radius: 18px;
  background: #fff8e8;
  color: #202020;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  box-shadow: 0 0 0 2px #202020, 6px 6px 0 2px #202020;
}
.fc-quiz__face p {
  margin: 0;
}
.fc-quiz__face--back {
  justify-content: center;
  background: #2f7a4b;
  color: #fff;
  transform: rotateY(180deg);
}
.fc-quiz.is-wrong .fc-quiz__face--back {
  background: #a83a1c;
}
.fc-quiz__set {
  display: grid;
  gap: 8px;
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}
.fc-quiz__q {
  margin-bottom: 8px;
  padding: 0;
  font: 600 19px/1.3 "Fraunces", Georgia, serif;
}
.fc-quiz__opt {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  padding: 0 12px;
  border: 2px solid #202020;
  border-radius: 10px;
  background: #fff;
  font-size: 14px;
  cursor: pointer;
}
.fc-quiz__opt:has(input:checked) {
  background: #ffd23f;
}
.fc-quiz__opt input {
  width: 18px;
  height: 18px;
  margin: 0;
  accent-color: #202020;
}
.fc-quiz__opt code {
  font: 13px/1.3 ui-monospace, Consolas, monospace;
  overflow-wrap: anywhere;
}
.fc-quiz__result {
  font: 700 34px/1.1 "Fraunces", Georgia, serif;
}
.fc-quiz__result:focus {
  outline: none;
}
.fc-quiz__result:focus-visible {
  outline: 3px solid #fff;
  outline-offset: 4px;
}
.fc-quiz__why {
  margin-top: 12px !important;
  font-size: 15px;
  line-height: 1.5;
}
.fc-quiz__btn {
  min-height: 44px;
  margin-top: auto;
  border: 2px solid #202020;
  border-radius: 10px;
  background: #202020;
  color: #fff8e8;
  font: inherit;
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
}
.fc-quiz__face--back .fc-quiz__btn {
  margin-top: 24px;
  border-color: #fff;
  background: #fff;
  color: #202020;
}
.fc-quiz__btn:focus-visible,
.fc-quiz__opt:has(input:focus-visible) {
  outline: 3px solid #1d3fbf;
  outline-offset: 2px;
}
.fc-quiz__face--back .fc-quiz__btn:focus-visible {
  outline-color: #fff;
}
@media (prefers-reduced-motion: no-preference) {
  .fc-quiz__card {
    transition: transform .7s cubic-bezier(.3, .7, .2, 1);
  }
}

Staggered grid flip

  • transition-delay
  • --fc-i index
  • aria-pressed

Coffee bar

  • EspressoSingle origin
    €2.40Double €3.10
  • Flat whiteWhole or oat
    €3.608 oz
  • CortadoEqual parts
    €3.204.5 oz
  • FilterRotating beans
    €3.00Free refill
  • Cold brewSteeped 18 h
    €4.1012 oz
  • ChaiHouse spice
    €3.80Hot or iced

One switch flips the whole menu board. Each card gets its index as a custom property, and transition-delay multiplies it so the cards turn in a wave. The grid reflows from three to two columns with auto-fit, and all six back faces go inert together when the switch is off.

View code
.fc-menu {
  width: min(460px, 100%);
  box-sizing: border-box;
  padding: 20px;
  border-radius: 20px;
  background: #2b1d16;
  color: #f6ead8;
}
.fc-menu__head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  margin-bottom: 16px;
}
.fc-menu__title {
  margin: 0;
  font: 600 24px/1.1 "Fraunces", Georgia, serif;
}
.fc-menu__toggle {
  min-height: 44px;
  padding: 0 16px;
  border: 2px solid #e9b872;
  border-radius: 99px;
  background: transparent;
  color: #f6ead8;
  font: inherit;
  font-size: 14px;
  font-weight: 700;
  cursor: pointer;
}
.fc-menu__toggle[aria-pressed="true"] {
  background: #e9b872;
  color: #2b1d16;
}
.fc-menu__toggle:focus-visible {
  outline: 3px solid #fff;
  outline-offset: 3px;
}
.fc-menu__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(118px, 1fr));
  gap: 10px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.fc-menu__item {
  margin: 0;
  perspective: 600px;
}
.fc-menu__card {
  position: relative;
  height: 92px;
  transform-style: preserve-3d;
}
.fc-menu.is-flipped .fc-menu__card {
  transform: rotateY(180deg);
}
.fc-menu__face {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  box-sizing: border-box;
  padding: 10px 12px;
  border-radius: 12px;
  background: #3d2a20;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
}
.fc-menu__face b {
  font-size: 16px;
}
.fc-menu__face span {
  margin-top: 2px;
  color: #d8c6ae;
  font-size: 13px;
}
.fc-menu__face--back {
  background: #f6ead8;
  color: #2b1d16;
  transform: rotateY(180deg);
}
.fc-menu__face--back b {
  font: 600 22px/1.1 "Fraunces", Georgia, serif;
  font-variant-numeric: tabular-nums;
}
.fc-menu__face--back span {
  color: #5c4637;
}
@media (prefers-reduced-motion: no-preference) {
  .fc-menu__card {
    transition: transform .5s cubic-bezier(.3, .7, .2, 1);
    transition-delay: calc(var(--fc-i, 0) * 70ms);
  }
}

Transit pass in a phone

  • height: 100%
  • aria-pressed
  • inert

Wallet

Ostra Transit

OSTRA

Zones 1-3

Monthly pass

Leah Brandt

Valid to 30 Nov 2026

No. 4471 0932

QR code for pass 4471 0932

Hold to the gate reader

  • Trips this month38
  • Renews1 Dec
  • PaymentCard ending 21

A wallet screen where the pass turns over to show its QR code for the gate reader. The card sizes itself from the screen width with aspect-ratio, the app layout fills the phone with height: 100%, and the toggle under the card keeps focus in one place while the faces swap.

View code
.fc-pass {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 16px;
  height: 100%;
  box-sizing: border-box;
  padding: 22px 18px;
  overflow: hidden;
  background: #f1f2f6;
  color: #12141c;
}
.fc-pass p {
  margin: 0;
}
.fc-pass__app {
  font-size: 26px;
  font-weight: 800;
  letter-spacing: -.02em;
}
.fc-pass__sub {
  color: #555b6e;
  font-size: 14px;
}
.fc-pass__view {
  perspective: 1000px;
}
.fc-pass__card {
  position: relative;
  aspect-ratio: 1.45;
  max-height: 260px;
  transform-style: preserve-3d;
}
.fc-pass__card.is-flipped {
  transform: rotateY(180deg);
}
.fc-pass__face {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  padding: 16px 18px;
  border-radius: 18px;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  box-shadow: 0 14px 28px rgba(18, 20, 28, .2);
}
.fc-pass__face--front {
  background: #0f5c7a;
  color: #fff;
}
.fc-pass__face--back {
  align-items: center;
  justify-content: center;
  gap: 6px;
  background: #fff;
  color: #12141c;
  transform: rotateY(180deg);
}
.fc-pass__row {
  display: flex;
  justify-content: space-between;
  gap: 10px;
}
.fc-pass__row--end {
  margin-top: auto;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}
.fc-pass__brand {
  font-weight: 800;
  letter-spacing: .14em;
}
.fc-pass__zone {
  padding: 2px 8px;
  border-radius: 99px;
  background: #ffd166;
  color: #12141c;
  font-size: 12px;
  font-weight: 700;
}
.fc-pass__kind {
  margin-top: 14px !important;
  font: 600 22px/1.1 "Fraunces", Georgia, serif;
}
.fc-pass__holder {
  margin-top: 4px !important;
  font-size: 14px;
}
.fc-pass__qr {
  display: block;
  width: auto;
  height: 74%;
  aspect-ratio: 1;
}
.fc-pass__scan {
  font-size: 13px;
  font-weight: 600;
}
.fc-pass__btn {
  min-height: 48px;
  border: 0;
  border-radius: 14px;
  background: #12141c;
  color: #fff;
  font: inherit;
  font-size: 16px;
  font-weight: 700;
  cursor: pointer;
}
.fc-pass__btn:focus-visible {
  outline: 3px solid #0f5c7a;
  outline-offset: 3px;
}
.fc-pass__list {
  margin: 0;
  padding: 4px 16px;
  border-radius: 16px;
  background: #fff;
  list-style: none;
}
.fc-pass__list li {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 11px 0;
  border-bottom: 1px solid #e3e5ec;
  font-size: 14px;
}
.fc-pass__list li:last-child {
  border-bottom: 0;
}
.fc-pass__list span {
  color: #555b6e;
}
@media (prefers-reduced-motion: no-preference) {
  .fc-pass__card {
    transition: transform .65s cubic-bezier(.3, .7, .2, 1);
  }
}

What Is a CSS Flip Card?

The front usually carries an image or a headline, and the back carries the detail, like a bio, a price, a definition or a spec. Point at the card (or tap it, or press a key, depending on how it’s wired) and it turns over in 3D to show the other side. CSS transforms and a transition do all of it, with no animation library involved.

Structurally there’s a container that sets the card size and holds the perspective. Inside it sits a wrapper, and the wrapper is the element that actually rotates. The front face is what you see at rest. The back face is pre-rotated 180 degrees, so it stays hidden until the flip.

W3Schools names these .flip-card, .flip-card-inner, .flip-card-front and .flip-card-back, and a Leyaa guide uses the same scheme. Other tutorials pick their own class names.

The whole structure is plain HTML. Four elements, no script, no required images.

This isn’t a fade or a slide between two panels, either. MDN notes that backface-visibility has no effect on 2D transforms, which means a flat rotate with no perspective can’t give you the two-sided look.

How to Build a CSS Flip Card Step by Step

Work through these in order. Skipping one tends to fail without any error, which makes it annoying to debug.

  1. Give .flip-card a width, a height and perspective: 1000px.
  2. Give .flip-card-inner position: relative, transition: transform 0.6s and transform-style: preserve-3d.
  3. Set both faces to position: absolute, full width and height, with backface-visibility: hidden.
  4. Add transform: rotateY(180deg) to .flip-card-back.
  5. Rotate the wrapper to rotateY(180deg) inside a trigger rule such as .flip-card:hover .flip-card-inner.

Markup first.

<div class="flip-card">
  <div class="flip-card-inner">
    <div class="flip-card-front">Front</div>
    <div class="flip-card-back">Back</div>
  </div>
</div>

And the styles.

.flip-card {
  width: 300px;
  height: 200px;
  perspective: 1000px;
}

.flip-card-inner {
  position: relative;
  width: 100%;
  height: 100%;
  transition: transform 0.6s;
  transform-style: preserve-3d;
}

.flip-card:hover .flip-card-inner {
  transform: rotateY(180deg);
}

.flip-card-front,
.flip-card-back {
  position: absolute;
  width: 100%;
  height: 100%;
  -webkit-backface-visibility: hidden; /* Safari */
  backface-visibility: hidden;
}

.flip-card-back {
  transform: rotateY(180deg);
}

The perspective property goes on the container, not on the wrapper that rotates. The fullPage card flip guide points out that setting it on the inner wrapper has no effect on that wrapper’s own rotation.

Skip step 2 and both faces collapse into one plane. The fullPage guide and a Leyaa guide both flag this failure. The container also needs a defined width and height, since Metro UI’s flip card docs list that as a requirement.

That -webkit- line is a Safari leftover, needed only before Safari 15.4 (March 2022). W3Schools still ships it in its flip card example as of October 2026.

Which Perspective, Duration and Axis Values Work Best?

Start with a perspective around 3 to 4 times the card width, a transition between 0.3s and 0.8s, and rotateY for a side-to-side turn. The tutorials that state exact values all land inside those ranges.

Here’s what the published ones actually use, as read in October 2026.

  • W3Schools sets perspective at 1000px with a 0.8s transition on a card that’s 300 by 200 pixels.
  • The DEV Community tutorial goes with perspective 600px and a 0.4s transition (0.3s on the hover rule), on a card 150 by 150 pixels.
  • The fullPage card flip guide calls about 800px a sweet spot and suggests 300 to 500px for a dramatic rotation.
  • Metro UI’s flip card defaults to an animation speed of 0.3s.

Divide each perspective by its card width and a pattern shows up: 3.3 for W3Schools, 4 for the DEV Community tutorial. Two data points make a starting range, not a law.

MDN covers which way the effect runs. A large perspective value gives a small transformation, and a small value gives a big one.

Horizontal or vertical flip

For a horizontal flip, put rotateY(180deg) on the wrapper and pre-rotate the back face by the same rotateY(180deg). A vertical flip swaps both rotations to rotateX(180deg), and Daily Dev Tips publishes a vertical variant next to its horizontal one. Use a negative angle such as rotateY(-180deg) and the card turns the other way, as the fullPage guide explains.

Keep the back face pre-rotation on the same axis as the trigger rotation. Mix them and the back text reads mirrored.

Easing and duration

Duration sets how long the turn takes, and the timing function sets how the speed changes along the way. Preview both in the browser before you commit values to the stylesheet.

MDN’s keyword options for transition-timing-function include ease, ease-in, ease-out, ease-in-out and linear.

A Treehouse CSS challenge uses ease-in-out as the timing function that starts and stops gradually, which suits a card that settles into place. Stay at or under 0.8s, since that’s the longest value among the sources above and anything slower drags.

Hover, Focus, Click or Checkbox: Which Trigger Fits?

Pick the trigger by the weakest input device your visitors use. A class toggle on a real button covers mouse, touch and keyboard, while a hover-only flip fails on touch screens.

TriggerTouch screensKeyboardNeeds JavaScript
:hoverUnreliableNoNo
:focus-withinBrowser dependentYes, with tabindexNo
Checkbox hackYes, tap the labelYes, if the input stays focusableNo
Class toggleYesYes, on a buttonYes

Hover is the simplest of the CSS hover effects to wire up, a single rule on the container.

The Calgary design system warns against relying on hover alone, and the fullPage guide notes that the :hover selector does not work on touch devices. A June 2021 CSS-IRL article adds a wrinkle. Some Android versions emulate hover on a long press, so (hover: hover) alone can evaluate true on a phone. Pair it with (pointer: fine).

Focus is the next option. The :focus-within pseudo-class matches an element when it or any descendant has focus, and MDN lists it as available across browsers since January 2020. A div isn’t focusable by default, so the card needs tabindex="0" (Quackit’s 3D card flip example does this).

W3C’s Understanding document for WCAG 2.2 criterion 1.4.13 states that content triggered by pointer hover should also be triggerable by keyboard focus.

The checkbox hack flips the card on tap with no script at all. Wrap the card in a label tied to a hidden checkbox, then use a :checked sibling selector to rotate it. FreeFrontend lists several pure CSS cards and decks built this way.

Hide the input visually, not with display: none, or it drops out of the tab order.

With a class toggle, a few lines of JavaScript add a .flipped class on click, and the CSS rule mirrors the hover rule. The fullPage guide recommends this route when click-to-flip does not work with CSS alone.

So hover only makes sense when the back face holds nothing a visitor needs. For anything that matters, use a class toggle on a button. If scripts are off the table, focus-within or the checkbox hack will do.

How to Make a CSS Flip Card Accessible

No single attribute makes a flip card accessible. A keyboard has to be able to trigger it, a screen reader has to know which face is showing, the inactive face shouldn’t be read aloud, and visitors who ask for less motion shouldn’t get the rotation.

Keyboard and focus

The City of Calgary design system tells teams to give each interactive card role="button" or role="group", add tabindex="0", and make Enter and Space flip it.

James Edwards, writing for Vispero in September 2025, takes a different line. He says never to put role="button" on the card or its face containers, because it cancels the semantics of everything inside, such as headings and links, for assistive technology. A real button inside the card is the better route. A focusable div with a button role looks simpler, but it hides the content the card exists to show.

Sources also split on state. A Colorlib template released in September 2026 exposes the flip through a toggle button with aria-pressed, while Vispero found that ARIA state attributes such as aria-pressed and aria-expanded do not describe a card that is never pressed or expanded. Regular screen reader users who tested demos built on them found them confusing.

Vispero’s fix is a button whose label alternates between “Flip to back” and “Flip to front”, plus a description of the face now showing. iOS VoiceOver does not support dynamic description updates, so a static aria-describedby stays as the fallback.

Screen readers and the hidden face

Give the inactive face display: none, which removes it from the accessibility tree. Vispero does this, and the Colorlib template also removes its hidden face from that tree. backface-visibility: hidden only controls painting, so it can’t do that job alone.

Vispero also swaps the visible face at the 90 degree point of the rotation, when neither face can be seen.

That last trick means only one face exists at a time, so the faces need no absolute positioning, no fixed height and no matching size. The price is a JavaScript class that watches the rotation with requestAnimationFrame() and getComputedStyle().

Reduced motion

MDN names scaling and panning of large objects as vestibular motion triggers, and a rotating card is a large object in motion.

A media query on prefers-reduced-motion lets those visitors skip the rotation while everyone else keeps it.

@media (prefers-reduced-motion: reduce) {
  .flip-card-inner {
    transition: none;
  }
}

That snippet is the snap approach. The flip still happens, instantly, and Quackit’s 3D card flip example works this way. Crossfading swaps the faces into each other instead of rotating, which is what the Colorlib template does. Gating puts the rotation rules only inside (prefers-reduced-motion: no-preference), so a plain face swap is the default, and Vispero builds it this way.

Gating is the safest of the three, because a browser that cannot evaluate the query never plays the rotation.

How to Lay Out Flip Cards in a Responsive Grid

A CSS grid with repeat(auto-fit, minmax(240px, 1fr)) columns does most of the work. Each card gets its height from its width through aspect-ratio, and the columns reflow on their own as the viewport narrows.

.flip-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 1.5rem;
}

.flip-card {
  aspect-ratio: 3 / 2;
  perspective: 1000px;
}

The faces are absolutely positioned, so the card needs a height the grid can read. The aspect ratio supplies one from the column width, and a fixed height works too.

No breakpoint decides the column count here, which makes it the lowest-effort route to responsive design for a card collection.

Vispero’s review of flip card implementations says absolute positioning with fixed sizes stops the content from responding to larger fonts or text spacing, so one or both faces overflow the container. The City of Calgary design system caps rotating cards at 4 per row, because crowded layouts hurt focus and mobile usability.

Perspective units matter too. A pixel value suits one card width, and grid columns stretch. Vispero sets perspective: 64rem, which follows font size instead of column width.

Measure the longest back face before choosing the ratio. If it needs more room than the ratio allows, shorten the copy or change the pattern.

Do CSS Flip Cards Work in Every Browser?

Chrome, Edge, Firefox and Safari all run a flip card without extra code. Can I use lists Internet Explorer 10 and 11 and Opera Mini as the gaps.

PropertyMDN Baseline statusJob in the card
perspectiveWidely available; across browsers since September 2015Depth on the container
transform-styleWidely available; across browsers since September 2015Keeps both faces in 3D
backface-visibilityWidely available; across browsers since March 2022Hides the reversed face

Can I use marks IE 10 and 11 as partial support in its 3D transforms entry, and its note explains the label: neither browser supports transform-style: preserve-3d.

A flip card depends on preserve-3d, so partial support in IE still means a broken card.

Treat the IE gap as a decision, not a bug. Cross-browser compatibility testing on the browsers you actually ship to tells you whether a fallback is worth building, such as swapping faces with plain display rules.

MDN’s compatibility notes also record that the perspective property did not function properly on iOS 13, with the issues fixed in iOS 14.

Since the unprefixed property has Baseline status, projects that target current browsers can drop -webkit-backface-visibility. Keep it only for Safari 9 to 15.3.

Why Is a CSS Flip Card Not Working?

Nearly every failure traces back to a broken 3D context. Usually the wrapper is flattened by another property, a nested element is missing preserve-3d, or a stacking context traps the faces.

SymptomCauseFix
Faces flat although preserve-3d is setWrapper has overflow other than visible or clip, opacity below 1, a filter, or isolation: isolateMove those properties to the container or a face
Faces flat after adding a wrappertransform-style is not inheritedSet preserve-3d on every non-leaf element above the faces
z-index ignored between cardsperspective and preserve-3d each create a stacking contextRaise the container, not its children
Back text spills out of the cardFixed height with absolutely positioned facesScroll inside the face with overflow-y: auto, never on the wrapper
Text looks soft in SafariInside a scaled parent, a 3D-rotated sibling makes text render as a scaled bitmapNo confirmed CSS fix

The first three rows come from MDN’s documentation of transform-style and perspective. The overflow row follows from the first, since the faces are leaf elements and the wrapper is not.

The Safari row rests on a WebKit Bugzilla report (278043, August 2024, Safari Technology Preview 199 on macOS 14.6). It lists the bug as new and notes that Chrome and Firefox render the same page sharply.

When a flip card is the wrong choice

  • Information visitors need sits on the back face, and keyboard and screen reader users cannot reach it (the City of Calgary design system advises against this).
  • The back content varies in length or has to survive text zoom, where an accordion reveals the same detail in normal flow.

If most visitors need the back face, put its content on the front.

CSS Flip Card FAQ

Can I build a CSS flip card with Tailwind CSS?

Yes. Tailwind CSS 4 covers every property the card needs. transform-3d sets preserve-3d, perspective-* sets depth, rotate-y-* turns the faces, and backface-hidden hides the reversed side. Earlier versions lack these 3D utilities.

Does Bootstrap include a flip card component?

No. Bootstrap’s card component is a content container with headers, footers and images, and its documentation describes no flip behavior.

Add the rotation with custom CSS on top of the card markup, and use Bootstrap’s grid to wrap the cards in columns.

Is the GSAP Flip plugin the same as a CSS flip card?

Only the name is shared. The GSAP Flip plugin records an element’s state, lets the DOM change, then animates the difference, which suits layout transitions in grid and flexbox.

A rotating card built with GSAP still tweens a rotation.

How do I put an image on the front and text on the back?

Place the img inside the front face and the heading and text inside the back face. W3Schools’ flip card example does exactly this, with an avatar in front and a name and title behind it.

Size the image to the face with object-fit: cover.

How do I make the card spin a full 360 degrees?

Change the trigger rotation from 180deg to 360deg. A DEV Community tutorial notes this suits single-sided buttons, because a two-faced card finishes the turn showing its front again and the back face appears only mid-spin.

Which content suits a CSS flip card?

Anything where the front identifies something and the back adds optional detail. Team member photos with a bio behind them are the classic case. Product images with specs work too, and so do flashcards that put a term on the front and its meaning on the back.

Can a CSS flip card flip on its own with keyframes?

Yes, an animation built from CSS keyframes can rotate the inner wrapper on a timer.

The City of Calgary design system advises against it, because motion that starts without user interaction is unpredictable and disorienting.

How to Test a CSS Flip Card Before Launch

Testing comes down to whether the back face stays reachable under every input and setting your visitors use. That means mouse, keyboard, touch, a screen reader and reduced motion.

Run the checks in order of how many visitors a failure blocks.

  • Tab to the card and press Enter and Space.
  • On a touch screen, tap once to flip and once to return.
  • Confirm a screen reader announces one face at a time.
  • Switch the operating system’s reduced motion setting on and flip again.

Reach comes before announcement and motion, since an unreachable back face is lost to a whole group of visitors however polished the animation is.

The cost is four passes per card pattern, and a grid only pays it once because every card shares the pattern.

Pages with more interactive components than cards can run the same passes through a web accessibility checklist.