CSS Perspective: How It Works + 17 3D Examples

Rotate a card with rotateY and it just looks like a flat shape getting narrower. CSS perspective sets the distance between the viewer and the z=0 plane, which gives the turn its depth.

Below are 17 CSS perspective examples you can try and copy, followed by a guide to the property, the perspective() function, perspective-origin and preserve-3d.

By Bogdan Sandu · Updated October 28, 2026

Perspective basics you can poke at

Four small labs for the parts of the topic that confuse people most: where the perspective lives, what perspective-origin moves, what quietly flattens preserve-3d, and what backface-visibility hides.

perspective property vs perspective() function

  • perspective
  • perspective()
  • shared vanishing point
Mon18° Tue21° Wed16°
perspective: 400px on the row. One vanishing point for all three tiles.
Mon18° Tue21° Wed16°
perspective(400px) inside each tile transform. Three separate vanishing points.

Drag the sliders or press the toggle buttons under each scene.

Both rows rotate three tiles by 45 degrees. On the left the row holds perspective: 400px, so all tiles share one vanishing point in the middle of the row and each one turns a little differently. On the right every tile carries perspective(400px) in its own transform, so each has its own vanishing point and the three look like identical copies.

View code
.pe-cmp {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 18px;
  width: 100%;
  max-width: 760px;
}
.pe-cmp__col {
  flex: 1 1 280px;
  max-width: 360px;
  margin: 0;
  padding: 26px 16px 18px;
  border-radius: 18px;
  background: #fff;
  box-shadow: 0 12px 30px rgba(15, 23, 42, .08);
}
.pe-cmp__row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  height: 120px;
}
.pe-cmp__row--shared {
  perspective: 400px;
}
.pe-cmp__tile {
  display: grid;
  place-content: center;
  gap: 4px;
  width: 70px;
  height: 96px;
  border-radius: 12px;
  text-align: center;
  font-size: 14px;
  line-height: 1.1;
}
.pe-cmp__tile b {
  font-weight: 700;
}
.pe-cmp__tile span {
  font: 600 22px/1 "Fraunces", Georgia, serif;
}
.pe-cmp__row--shared .pe-cmp__tile {
  background: #11475a;
  color: #fff;
  transform: rotateY(45deg);
}
.pe-cmp__row--own .pe-cmp__tile {
  background: #f3c9a8;
  color: #3d1a07;
  transform: perspective(400px) rotateY(45deg);
}
.pe-cmp__cap {
  margin: 14px 0 0;
  color: #3b4450;
  font-size: 14px;
  line-height: 1.5;
  text-align: center;
}
.pe-cmp__cap code {
  padding: 2px 6px;
  border-radius: 6px;
  background: #eef1f5;
  color: #11253a;
  font: 600 13px/1.4 ui-monospace, Consolas, monospace;
}

perspective-origin playground

  • perspective-origin
  • custom properties
  • range inputs
A deep blue fjord between pale granite cliffs

perspective: 420px;
perspective-origin: 50% 50%;

Drag the sliders or press the toggle buttons under each scene.

An open box built from five faces inside one preserve-3d wrapper. The sliders write three custom properties, and the scene reads them in perspective and perspective-origin, so moving X to the right shows more of the left wall, as if you stepped right. The overflow: hidden on the view is safe because that element only holds the perspective; it never carries preserve-3d itself.

View code
.pe-room {
  --pe-d: 420px;
  --pe-ox: 50%;
  --pe-oy: 50%;
  display: grid;
  justify-items: center;
  gap: 18px;
  width: 100%;
  max-width: 560px;
}
.pe-room__view {
  display: grid;
  place-items: center;
  width: 100%;
  height: 240px;
  overflow: hidden;
  border-radius: 18px;
  background: #1d2430;
  perspective: var(--pe-d);
  perspective-origin: var(--pe-ox) var(--pe-oy);
}
.pe-room__box {
  position: relative;
  width: 200px;
  height: 140px;
  transform-style: preserve-3d;
}
.pe-room__face {
  position: absolute;
  box-sizing: border-box;
}
.pe-room__face--back {
  inset: 0;
  padding: 8px;
  background: #f4efe6;
  transform: translateZ(-120px);
}
.pe-room__face--back img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.pe-room__face--left,
.pe-room__face--right {
  top: 0;
  width: 120px;
  height: 100%;
}
.pe-room__face--left {
  left: 0;
  background: #c7b394;
  transform-origin: left center;
  transform: rotateY(90deg);
}
.pe-room__face--right {
  right: 0;
  background: #a9916e;
  transform-origin: right center;
  transform: rotateY(-90deg);
}
.pe-room__face--floor,
.pe-room__face--ceil {
  left: 0;
  width: 100%;
  height: 120px;
}
.pe-room__face--floor {
  bottom: 0;
  background-color: #7a5d40;
  background-image: linear-gradient(90deg, rgba(0, 0, 0, .22) 1px, transparent 1px), linear-gradient(rgba(0, 0, 0, .22) 1px, transparent 1px);
  background-size: 25px 20px;
  transform-origin: center bottom;
  transform: rotateX(90deg);
}
.pe-room__face--ceil {
  top: 0;
  background: #e6dccb;
  transform-origin: center top;
  transform: rotateX(-90deg);
}
.pe-room__controls {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px 20px;
}
.pe-room__field {
  display: grid;
  gap: 4px;
  color: #26303d;
  font-size: 13px;
  font-weight: 600;
}
.pe-room__field input {
  width: 150px;
  height: 28px;
  margin: 0;
  accent-color: #7a4b22;
}
.pe-room__field input:focus-visible {
  outline: 3px solid #1847f0;
  outline-offset: 2px;
}
.pe-room__code {
  margin: 0;
  padding: 10px 14px;
  border-radius: 10px;
  background: #fff;
  color: #1b2430;
  font: 13px/1.6 ui-monospace, Consolas, monospace;
  text-align: center;
}
.pe-room__code code {
  font: inherit;
}
.pe-room__val {
  color: #8a3d0e;
  font-weight: 700;
}

What flattens preserve-3d

  • transform-style
  • grouping properties
  • translateZ
Header Nav Content

3D: the layers float above the plane.

Drag the sliders or press the toggle buttons under each scene.

Three layout layers float at different translateZ heights above a tilted plane, and the dashed outlines mark z = 0. Switch preserve-3d off, or switch overflow: hidden on for the plane, and the layers drop into the plane: overflow values other than visible or clip force the used value of transform-style to flat. Opacity below 1, filter, clip-path and mask do the same.

View code
.pe-flat {
  display: grid;
  justify-items: center;
  gap: 14px;
  width: 100%;
  max-width: 520px;
  color: #e9edf5;
}
.pe-flat__scene {
  display: grid;
  place-items: center;
  width: 100%;
  height: 250px;
  perspective: 700px;
}
.pe-flat__plane {
  position: relative;
  width: 220px;
  height: 150px;
  border: 1px solid #3a4a6b;
  border-radius: 10px;
  background: #18223a;
  transform-style: preserve-3d;
  transform: rotateX(58deg) rotateZ(-36deg);
}
.pe-flat[data-preserve="false"] .pe-flat__plane {
  transform-style: flat;
}
.pe-flat[data-overflow="true"] .pe-flat__plane {
  overflow: hidden;
}
.pe-flat__a {
  top: 12px;
  left: 12px;
  width: 196px;
  height: 30px;
}
.pe-flat__b {
  top: 52px;
  left: 12px;
  width: 52px;
  height: 86px;
}
.pe-flat__c {
  top: 52px;
  left: 74px;
  width: 134px;
  height: 86px;
}
.pe-flat__ghost {
  position: absolute;
  box-sizing: border-box;
  border: 1.5px dashed #6c7fa8;
  border-radius: 6px;
}
.pe-flat__chip {
  position: absolute;
  box-sizing: border-box;
  display: grid;
  place-items: center;
  border-radius: 6px;
  font-size: 12px;
  font-weight: 700;
  box-shadow: 0 6px 14px rgba(0, 0, 0, .35);
}
.pe-flat__chip.pe-flat__a {
  background: #ffb547;
  color: #241500;
  transform: translateZ(46px);
}
.pe-flat__chip.pe-flat__b {
  background: #5ad1b3;
  color: #04261d;
  transform: translateZ(26px);
}
.pe-flat__chip.pe-flat__c {
  background: #f2f4f8;
  color: #18223a;
  transform: translateZ(66px);
}
.pe-flat__controls {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px;
}
.pe-flat__btn {
  min-height: 44px;
  padding: 0 14px;
  border: 1.5px solid #6c7fa8;
  border-radius: 10px;
  background: transparent;
  color: #e9edf5;
  font: 600 13px/1 ui-monospace, Consolas, monospace;
  cursor: pointer;
  transition: background-color .2s, color .2s;
}
.pe-flat__btn[aria-pressed="true"] {
  border-color: #ffb547;
  background: #ffb547;
  color: #241500;
}
.pe-flat__btn[aria-pressed="false"] {
  text-decoration: line-through;
  text-decoration-thickness: 1.5px;
}
.pe-flat__btn:focus-visible {
  outline: 3px solid #8fb3ff;
  outline-offset: 3px;
}
.pe-flat__status {
  min-height: 3em;
  margin: 0;
  color: #c9d3e6;
  font-size: 14px;
  line-height: 1.5;
  text-align: center;
}

backface-visibility on a two-sided pass

  • backface-visibility
  • -webkit- prefix
  • rotateY

Northgull Air

OSL to BGO

Flight NG 214 · Boarding 07:40

Seat 14C · Gate B7 · Zone 2

Passenger L. Haugen

Drag the sliders or press the toggle buttons under each scene.

Front and back are two faces stacked in one preserve-3d card, with the back pre-rotated by 180 degrees. With backface-visibility: hidden each face disappears once it turns away; switch it off and the back face shows through as soon as the card turns, mirrored. The -webkit- line covers Safari versions before 15.4, which only knew the prefixed property.

View code
.pe-bf {
  --pe-a: 35deg;
  display: grid;
  justify-items: center;
  gap: 16px;
  width: 100%;
  max-width: 520px;
}
.pe-bf__scene {
  display: grid;
  place-items: center;
  width: 100%;
  height: 210px;
  perspective: 800px;
}
.pe-bf__card {
  position: relative;
  width: min(270px, 100%);
  height: 160px;
  transform-style: preserve-3d;
  transform: rotateY(var(--pe-a));
}
.pe-bf__face {
  position: absolute;
  inset: 0;
  box-sizing: border-box;
  display: grid;
  align-content: space-between;
  padding: 16px 18px;
  border-radius: 14px;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  box-shadow: 0 14px 30px rgba(15, 23, 42, .16);
}
.pe-bf[data-backface="visible"] .pe-bf__face {
  -webkit-backface-visibility: visible;
  backface-visibility: visible;
}
.pe-bf__face p {
  margin: 0;
}
.pe-bf__face--front {
  background: #0f3d63;
  color: #fff;
}
.pe-bf__face--back {
  background: #fff4dc;
  color: #2b2414;
  transform: rotateY(180deg);
}
.pe-bf__air {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: .04em;
}
.pe-bf__route {
  font: 600 34px/1 "Fraunces", Georgia, serif;
}
.pe-bf__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.pe-bf__meta {
  font-size: 13px;
}
.pe-bf__code {
  display: block;
  height: 46px;
  background: repeating-linear-gradient(90deg, #2b2414 0 2px, transparent 2px 5px, #2b2414 5px 6px, transparent 6px 9px);
}
.pe-bf__controls {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  justify-content: center;
  gap: 12px 18px;
}
.pe-bf__field {
  display: grid;
  gap: 4px;
  color: #26303d;
  font-size: 13px;
  font-weight: 600;
}
.pe-bf__val {
  color: #8a3d0e;
  font-family: ui-monospace, Consolas, monospace;
}
.pe-bf__field input {
  width: 170px;
  height: 28px;
  margin: 0;
  accent-color: #0f3d63;
}
.pe-bf__toggle {
  min-height: 44px;
  padding: 0 14px;
  border: 1.5px solid #0f3d63;
  border-radius: 10px;
  background: #fff;
  color: #0f3d63;
  font: 600 13px/1 ui-monospace, Consolas, monospace;
  cursor: pointer;
  transition: background-color .2s, color .2s;
}
.pe-bf__toggle[aria-pressed="true"] {
  background: #0f3d63;
  color: #fff;
}
.pe-bf__toggle[aria-pressed="false"] {
  text-decoration: line-through;
}
.pe-bf__field input:focus-visible,
.pe-bf__toggle:focus-visible {
  outline: 3px solid #1847f0;
  outline-offset: 3px;
}

Cards with real depth

Card patterns built on one perspective parent and a preserve-3d wrapper. Every turn has a button or focus state behind it, and the motion only runs for visitors who have not asked for reduced motion.

Tour card that tilts toward the pointer

  • perspective on the parent
  • translateZ layers
  • :focus-within
Red sandstone cliffs lit by low sun above a rocky canyon floor
2 spots left

Canyon rim, three days

Guided hike · Apr 18 to 20 · Small group

$640 per person

Hover, Tab to it, or press the button.

The wrapper holds perspective: 900px and the card holds preserve-3d, so the photo, badge and text can sit at different translateZ depths and separate as the card turns. A short script maps the pointer position to two custom properties, and keyboard users get a fixed tilt from :focus-within. The rounded photo clips with overflow: hidden on its own frame, a leaf element, because the same rule on the card would flatten it.

View code
.pe-tilt {
  padding: 10px 0;
  perspective: 900px;
}
.pe-tilt__card {
  --pe-rx: 6deg;
  --pe-ry: -10deg;
  position: relative;
  width: min(290px, 100%);
  border-radius: 20px;
  background: #fffaf2;
  color: #22201c;
  box-shadow: 0 16px 34px rgba(60, 40, 20, .14);
  transform-style: preserve-3d;
}
.pe-tilt__media {
  margin: 12px 12px 0;
  overflow: hidden;
  border-radius: 14px;
  aspect-ratio: 4 / 3;
}
.pe-tilt__media img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.pe-tilt__badge {
  position: absolute;
  top: 24px;
  left: 24px;
  padding: 6px 10px;
  border-radius: 999px;
  background: #1d2b25;
  color: #fff;
  font-size: 12px;
  font-weight: 700;
}
.pe-tilt__body {
  padding: 14px 18px 18px;
}
.pe-tilt__body p {
  margin: 0;
}
.pe-tilt__title {
  font: 600 21px/1.2 "Fraunces", Georgia, serif;
}
.pe-tilt__meta {
  margin-top: 4px !important;
  color: #5a5248;
  font-size: 13px;
}
.pe-tilt__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-top: 14px;
}
.pe-tilt__price {
  font-size: 20px;
  font-weight: 700;
}
.pe-tilt__price span {
  color: #5a5248;
  font-size: 12px;
  font-weight: 500;
}
.pe-tilt__cta {
  min-height: 44px;
  padding: 0 18px;
  border: 0;
  border-radius: 12px;
  background: #9c3b14;
  color: #fff;
  font: inherit;
  font-size: 15px;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
}
.pe-tilt__cta:focus-visible {
  outline: 3px solid #1847f0;
  outline-offset: 3px;
}
@media (prefers-reduced-motion: no-preference) {
  .pe-tilt__card,
  .pe-tilt__media,
  .pe-tilt__badge,
  .pe-tilt__body {
    transition: transform .45s cubic-bezier(.2, .7, .2, 1), box-shadow .45s;
  }
  .pe-tilt__card:hover,
  .pe-tilt__card:focus-within {
    transform: rotateX(var(--pe-rx)) rotateY(var(--pe-ry));
    box-shadow: 0 26px 50px rgba(60, 40, 20, .22);
  }
  .pe-tilt__card:hover .pe-tilt__media,
  .pe-tilt__card:focus-within .pe-tilt__media {
    transform: translateZ(26px);
  }
  .pe-tilt__card:hover .pe-tilt__badge,
  .pe-tilt__card:focus-within .pe-tilt__badge {
    transform: translateZ(60px);
  }
  .pe-tilt__card:hover .pe-tilt__body,
  .pe-tilt__card:focus-within .pe-tilt__body {
    transform: translateZ(18px);
  }
}

Accessible flip card

  • backface-visibility
  • rotateY(180deg)
  • inert

Ines Navarro

Product designer, Lisbon

Designs booking flows for small travel companies. Open to freelance work from March.

Email
ines@example.com
Studio
Rua das Flores 12, Lisbon
Hours
Mon to Thu, 9:00 to 17:00

Hover, Tab to it, or press the button.

The classic recipe: perspective on the container, preserve-3d on the card, two absolutely positioned faces with backface-visibility: hidden, and the back pre-rotated by 180 degrees. The buttons do the flipping, the face turned away gets the inert attribute so its button is skipped by Tab and screen readers, and focus moves to the button on the side that just turned to the front.

View code
.pe-flip {
  perspective: 1000px;
}
.pe-flip__card {
  position: relative;
  width: min(270px, 80vw);
  height: 330px;
  transform-style: preserve-3d;
}
.pe-flip__card.is-flipped {
  transform: rotateY(180deg);
}
.pe-flip__face {
  position: absolute;
  inset: 0;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  padding: 24px 22px;
  border-radius: 20px;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  box-shadow: 0 16px 34px rgba(30, 30, 20, .16);
}
.pe-flip__face p {
  margin: 0;
}
.pe-flip__face--front {
  background: #f4efe4;
  color: #1c1a17;
}
.pe-flip__face--back {
  background: #3f4d24;
  color: #f6f2e8;
  transform: rotateY(180deg);
}
.pe-flip__avatar {
  display: grid;
  place-items: center;
  width: 64px;
  height: 64px;
  margin-bottom: 14px;
  border-radius: 50%;
  background: #3f4d24;
  color: #f4efe4;
  font: 600 22px/1 "Fraunces", Georgia, serif;
}
.pe-flip__name {
  font: 600 24px/1.2 "Fraunces", Georgia, serif;
}
.pe-flip__role {
  margin-top: 2px !important;
  color: #4d4a43;
  font-size: 14px;
}
.pe-flip__bio {
  margin-top: 12px !important;
  font-size: 14px;
  line-height: 1.5;
}
.pe-flip__list {
  display: grid;
  gap: 14px;
  margin: 0;
}
.pe-flip__list dt {
  color: #d5dcbf;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
}
.pe-flip__list dd {
  margin: 2px 0 0;
  font-size: 15px;
  overflow-wrap: anywhere;
}
.pe-flip__btn {
  min-height: 44px;
  margin-top: auto;
  padding: 0 16px;
  border: 0;
  border-radius: 12px;
  background: #3f4d24;
  color: #fff;
  font: inherit;
  font-size: 14px;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
}
.pe-flip__face--back .pe-flip__btn {
  background: #f4efe4;
  color: #26301a;
}
.pe-flip__btn:focus-visible {
  outline: 3px solid #1847f0;
  outline-offset: 3px;
}
@media (prefers-reduced-motion: no-preference) {
  .pe-flip__card {
    transition: transform .7s cubic-bezier(.3, .7, .2, 1);
  }
}

Stacked 3D card deck

  • translateZ stacking
  • rotateX + rotateZ
  • aria-pressed

Everyday card selected.

Hover, Tab to it, or press the button.

Three payment cards sit on one tilted plane like steps, each card further back lifted a little higher with translateZ, so all three stay readable without box-shadow tricks. The plane keeps preserve-3d, so a hovered or focused card slides out along the plane with translate3d and the selected one slides out furthest. Selection is a set of aria-pressed buttons with a status line.

View code
.pe-deck {
  display: grid;
  justify-items: center;
  gap: 8px;
  width: 100%;
}
.pe-deck__scene {
  display: grid;
  place-items: center;
  width: 100%;
  height: 330px;
  perspective: 1000px;
}
.pe-deck__stack {
  position: relative;
  width: 200px;
  height: 264px;
  transform-style: preserve-3d;
  transform: translateX(-16px) rotateX(50deg) rotateZ(-30deg);
}
.pe-deck__card {
  --pe-z: 0px;
  position: absolute;
  left: 0;
  box-sizing: border-box;
  display: grid;
  grid-template-rows: auto 1fr auto;
  width: 200px;
  height: 124px;
  padding: 14px 16px;
  border: 0;
  border-radius: 14px;
  text-align: left;
  font: inherit;
  cursor: pointer;
  box-shadow: 0 2px 0 rgba(0, 0, 0, .25);
  transform: translateZ(var(--pe-z));
}
.pe-deck__card--1 {
  --pe-z: 0px;
  top: 140px;
  background: #e8604c;
  color: #1d0905;
}
.pe-deck__card--2 {
  --pe-z: 26px;
  top: 70px;
  background: #f3e6c8;
  color: #1d1a12;
}
.pe-deck__card--3 {
  --pe-z: 52px;
  top: 0;
  background: #24675a;
  color: #fff;
}
.pe-deck__card:hover,
.pe-deck__card:focus-visible {
  transform: translate3d(12px, 0, calc(var(--pe-z) + 4px));
}
.pe-deck__card[aria-pressed="true"] {
  transform: translate3d(30px, 0, calc(var(--pe-z) + 10px));
  box-shadow: 0 0 0 3px #fff;
}
.pe-deck__card:focus-visible {
  outline: 3px solid #8fb3ff;
  outline-offset: 4px;
}
.pe-deck__brand {
  font: 600 15px/1 "Fraunces", Georgia, serif;
}
.pe-deck__label {
  align-self: center;
  font-size: 17px;
  font-weight: 700;
}
.pe-deck__num {
  font: 600 14px/1 ui-monospace, Consolas, monospace;
  letter-spacing: .06em;
}
.pe-deck__status {
  margin: 0;
  color: #d4dbe8;
  font-size: 14px;
}
@media (prefers-reduced-motion: no-preference) {
  .pe-deck__card {
    transition: transform .35s cubic-bezier(.2, .7, .2, 1);
  }
}

Book cover with a spine and page edges

  • rotateY(-90deg) translateZ
  • preserve-3d
  • writing-mode

The Quiet Coast

Mara Ellison

Hover, Tab to it, or press the button.

A book is a box with four visible faces: the cover, the back, a spine and the page block. Spine and pages start at the center, rotate 90 degrees and then move out by half the cover width with translateZ, the same trick a cube uses. The button turns the book so the spine faces the reader, which is how a bookshop shelf view would show it.

View code
.pe-book {
  display: grid;
  justify-items: center;
  gap: 14px;
}
.pe-book__scene {
  display: grid;
  place-items: center;
  width: 280px;
  max-width: 100%;
  height: 290px;
  perspective: 1000px;
}
.pe-book__obj {
  position: relative;
  width: 170px;
  height: 250px;
  transform-style: preserve-3d;
  transform: rotateY(-28deg);
}
.pe-book:has([aria-pressed="true"]) .pe-book__obj {
  transform: rotateY(64deg);
}
.pe-book__cover,
.pe-book__back,
.pe-book__spine,
.pe-book__pages {
  position: absolute;
  top: 0;
  box-sizing: border-box;
  height: 100%;
}
.pe-book__cover {
  left: 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  width: 100%;
  padding: 20px 18px;
  border-radius: 2px 6px 6px 2px;
  background: #1f4d3f;
  color: #f6eedc;
  box-shadow: inset 0 0 0 6px #1f4d3f, inset 0 0 0 7px #c9b98f;
  transform: translateZ(17px);
}
.pe-book__cover p {
  margin: 0;
}
.pe-book__sun {
  width: 54px;
  height: 54px;
  margin: 0 0 auto;
  border-radius: 50%;
  background: #e9a23b;
}
.pe-book__title {
  font: 600 26px/1.05 "Fraunces", Georgia, serif;
}
.pe-book__author {
  margin-top: 8px !important;
  color: #d8ccb0;
  font-size: 13px;
  letter-spacing: .08em;
  text-transform: uppercase;
}
.pe-book__back {
  left: 0;
  width: 100%;
  border-radius: 6px 2px 2px 6px;
  background: #183d32;
  transform: rotateY(180deg) translateZ(17px);
}
.pe-book__spine {
  left: calc(50% - 17px);
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 34px;
  padding: 16px 0;
  background: #173b30;
  color: #f6eedc;
  font: 600 13px/1 "Fraunces", Georgia, serif;
  writing-mode: vertical-rl;
  transform: rotateY(-90deg) translateZ(85px);
}
.pe-book__pages {
  left: calc(50% - 16px);
  width: 32px;
  height: calc(100% - 6px);
  top: 3px;
  background: repeating-linear-gradient(90deg, #f5f0e4 0 2px, #ddd3c0 2px 3px);
  transform: rotateY(90deg) translateZ(84px);
}
.pe-book__btn {
  min-height: 44px;
  padding: 0 18px;
  border: 1.5px solid #1f4d3f;
  border-radius: 12px;
  background: #fff;
  color: #1f4d3f;
  font: inherit;
  font-size: 14px;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
}
.pe-book__btn[aria-pressed="true"] {
  background: #1f4d3f;
  color: #fff;
}
.pe-book__btn:focus-visible {
  outline: 3px solid #1847f0;
  outline-offset: 3px;
}
@media (prefers-reduced-motion: no-preference) {
  .pe-book__obj {
    transition: transform .8s cubic-bezier(.3, .7, .2, 1);
  }
}

3D objects and scenes

Cubes, rings, folds and doors: objects made of several faces that share one 3D rendering context. All of them sit still until you press a control, and none of them autoplays.

Feature cube with face buttons

  • six faces
  • translateZ(half the size)
  • aria-pressed
BackupsEvery hour
SyncAll devices
SharingExpiring links
Restore30-day history
Storage2 TB
SupportLive chat

Use the buttons under each scene.

Six faces, each rotated into place and pushed out by half the cube width with translateZ(75px). The cube keeps a fixed three-quarter view from the first two rotations, and two custom properties hold the extra turn for the chosen face, so the transform list always has the same shape and interpolates cleanly. Nothing spins on its own.

View code
.pe-cube {
  display: grid;
  justify-items: center;
  gap: 18px;
  width: 100%;
  max-width: 420px;
}
.pe-cube__scene {
  display: grid;
  place-items: center;
  width: 100%;
  height: 250px;
  perspective: 800px;
}
.pe-cube__box {
  --pe-tx: 0deg;
  --pe-ty: 0deg;
  position: relative;
  width: 150px;
  height: 150px;
  transform-style: preserve-3d;
  transform: rotateX(-16deg) rotateY(-26deg) rotateX(var(--pe-tx)) rotateY(var(--pe-ty));
}
.pe-cube__box[data-face="right"] { --pe-ty: -90deg; }
.pe-cube__box[data-face="back"] { --pe-ty: -180deg; }
.pe-cube__box[data-face="left"] { --pe-ty: 90deg; }
.pe-cube__box[data-face="top"] { --pe-tx: -90deg; }
.pe-cube__box[data-face="bottom"] { --pe-tx: 90deg; }
.pe-cube__face {
  position: absolute;
  inset: 0;
  box-sizing: border-box;
  display: grid;
  place-content: center;
  gap: 4px;
  border: 2px solid rgba(20, 22, 28, .25);
  border-radius: 6px;
  color: #14161c;
  text-align: center;
}
.pe-cube__face b {
  font: 600 22px/1.1 "Fraunces", Georgia, serif;
}
.pe-cube__face span {
  font-size: 13px;
  font-weight: 600;
}
.pe-cube__face--front { background: #ffcf5c; transform: translateZ(75px); }
.pe-cube__face--right { background: #7bd3ea; transform: rotateY(90deg) translateZ(75px); }
.pe-cube__face--back { background: #f29b77; transform: rotateY(180deg) translateZ(75px); }
.pe-cube__face--left { background: #a6e3a1; transform: rotateY(-90deg) translateZ(75px); }
.pe-cube__face--top { background: #cdbcff; transform: rotateX(90deg) translateZ(75px); }
.pe-cube__face--bottom { background: #eef0f4; transform: rotateX(-90deg) translateZ(75px); }
.pe-cube__controls {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
  width: 100%;
  max-width: 340px;
}
.pe-cube__btn {
  min-height: 44px;
  padding: 0 8px;
  border: 1.5px solid #4a5570;
  border-radius: 10px;
  background: transparent;
  color: #e9edf5;
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  line-height: 1;
  cursor: pointer;
  transition: background-color .2s, color .2s;
}
.pe-cube__btn[aria-pressed="true"] {
  border-color: #ffcf5c;
  background: #ffcf5c;
  color: #14161c;
}
.pe-cube__btn:focus-visible {
  outline: 3px solid #8fb3ff;
  outline-offset: 3px;
}
@media (prefers-reduced-motion: no-preference) {
  .pe-cube__box {
    transition: transform .8s cubic-bezier(.3, .7, .2, 1);
  }
}

Tri-fold brochure

  • transform-origin on the fold
  • nested preserve-3d
  • transition-delay

Hours

Daily 9:00 to 18:00

Glasshouse closes at 17:00

Find us

4 Orchard Lane

Bus 12 to Mill Gate

Spring walks

Magnolia path, 20 min

Fern gully, 35 min

Lake loop, 50 min

Tickets

Adults 12

Under 16 free

Lumen Gardens

Spring guide

Use the buttons under each scene.

Each wing pivots on the edge it shares with the middle panel, set with transform-origin, and each wing is itself preserve-3d with an inside and an outside face, so the cover appears when the brochure closes. The right wing waits for the left one with transition-delay, the way a real tri-fold closes, and a translateZ of 1px and 2px on the folded wings keeps the three panels from sharing a plane, which would make them flicker or cut through each other.

View code
.pe-fold {
  display: grid;
  justify-items: center;
  gap: 16px;
  width: 100%;
}
.pe-fold__scene {
  display: grid;
  place-items: center;
  width: 100%;
  height: 240px;
  perspective: 1100px;
}
.pe-fold__sheet {
  display: flex;
  transform-style: preserve-3d;
  transform: rotateX(10deg);
}
.pe-fold__panel {
  position: relative;
  width: 102px;
  height: 190px;
  transform-style: preserve-3d;
}
.pe-fold__panel--left {
  transform-origin: right center;
  transform: rotateY(22deg) translateZ(0);
}
.pe-fold__panel--right {
  transform-origin: left center;
  transform: rotateY(-22deg) translateZ(0);
}
.pe-fold.is-folded .pe-fold__panel--left {
  transform: rotateY(180deg) translateZ(-1px);
}
.pe-fold.is-folded .pe-fold__panel--right {
  transform: rotateY(-180deg) translateZ(-2px);
}
.pe-fold__side {
  position: absolute;
  inset: 0;
  box-sizing: border-box;
  padding: 14px 10px;
  background: #fbf7ee;
  color: #24301f;
  font-size: 12px;
  line-height: 1.45;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  box-shadow: inset 0 0 0 1px rgba(36, 48, 31, .12);
}
.pe-fold__side p {
  margin: 0 0 4px;
}
.pe-fold__panel--mid .pe-fold__side {
  background: #f3ecdc;
}
.pe-fold__side--out {
  background: #e7dcc4;
  transform: rotateY(180deg);
}
.pe-fold__side--cover {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  background: #2f5130;
  color: #f6f0df;
}
.pe-fold__h {
  margin-bottom: 8px !important;
  font: 600 15px/1.15 "Fraunces", Georgia, serif;
}
.pe-fold__brand {
  font: 600 20px/1.05 "Fraunces", Georgia, serif;
}
.pe-fold__btn {
  min-height: 44px;
  padding: 0 18px;
  border: 1.5px solid #2f5130;
  border-radius: 12px;
  background: #fff;
  color: #2f5130;
  font: inherit;
  font-size: 14px;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
}
.pe-fold__btn[aria-pressed="true"] {
  background: #2f5130;
  color: #fff;
}
.pe-fold__btn:focus-visible {
  outline: 3px solid #1847f0;
  outline-offset: 3px;
}
@media (prefers-reduced-motion: no-preference) {
  .pe-fold__panel--left,
  .pe-fold__panel--right {
    transition: transform .6s cubic-bezier(.4, .1, .2, 1);
  }
  .pe-fold__panel--left {
    transition-delay: .45s;
  }
  .pe-fold.is-folded .pe-fold__panel--left {
    transition-delay: 0s;
  }
  .pe-fold.is-folded .pe-fold__panel--right {
    transition-delay: .45s;
  }
}

Door that swings open on a reward

  • transform-origin: left
  • rotateY(-105deg)
  • aria-expanded

20% off winter boots

Use code FROST20

Valid today until midnight

Use the buttons under each scene.

An advent-style door: the frame holds the perspective, the door is a button hinged on its left edge with transform-origin, and opening it rotates the door past 90 degrees so its back panel shows. The door has a front and a back face, so the number never appears mirrored. The reward behind it is inert until the door opens, and aria-expanded tells screen reader users the state.

View code
.pe-door {
  padding: 10px 0;
}
.pe-door__frame {
  position: relative;
  width: 220px;
  height: 290px;
  padding: 0;
  border: 10px solid #3b2a1f;
  border-radius: 8px;
  background: #f7efe1;
  perspective: 900px;
}
.pe-door__inside {
  display: grid;
  place-content: center;
  gap: 8px;
  height: 100%;
  padding: 18px;
  box-sizing: border-box;
  color: #2a1d14;
  text-align: center;
}
.pe-door__inside p {
  margin: 0;
}
.pe-door__offer {
  font: 600 26px/1.1 "Fraunces", Georgia, serif;
}
.pe-door__code {
  font-size: 15px;
}
.pe-door__code b {
  padding: 2px 6px;
  border: 1.5px dashed #8c2f24;
  border-radius: 6px;
  color: #8c2f24;
  font-family: ui-monospace, Consolas, monospace;
}
.pe-door__small {
  color: #5c4a3c;
  font-size: 12px;
}
.pe-door__leaf {
  position: absolute;
  inset: 0;
  padding: 0;
  border: 0;
  background: transparent;
  font: inherit;
  cursor: pointer;
  transform-origin: left center;
  transform-style: preserve-3d;
}
.pe-door__leaf[aria-expanded="true"] {
  transform: rotateY(-105deg);
}
.pe-door__front,
.pe-door__back {
  position: absolute;
  inset: 0;
  border-radius: 3px;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
}
.pe-door__front {
  display: grid;
  place-content: center;
  justify-items: center;
  gap: 6px;
  background: #a8322a;
  color: #fff;
  box-shadow: inset 0 0 0 12px #a8322a, inset 0 0 0 14px #d9665b;
}
.pe-door__back {
  background: #7d241e;
  transform: rotateY(180deg);
}
.pe-door__num {
  font: 600 72px/1 "Fraunces", Georgia, serif;
}
.pe-door__label {
  font-size: 13px;
  font-weight: 700;
}
.pe-door__knob {
  position: absolute;
  top: 50%;
  right: 22px;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: #f1c56b;
}
.pe-door__leaf:focus-visible {
  outline: 3px solid #8fb3ff;
  outline-offset: 4px;
}
@media (prefers-reduced-motion: no-preference) {
  .pe-door__leaf {
    transition: transform .9s cubic-bezier(.3, .7, .2, 1);
  }
}

Isometric dashboard tiles

  • no perspective
  • rotateX(54.74deg) rotateZ(-45deg)
  • translateZ lift

Revenue: $48,210 this week, up 6% on last week.

Use the buttons under each scene.

Isometric drawings have no vanishing point, so this is the one demo with no perspective at all: rotateZ(-45deg) turns the grid into a diamond and rotateX(54.74deg) tilts it to a true isometric angle (60 degrees gives the 2:1 pixel-art look). preserve-3d still matters, because the chosen tile lifts with translateZ and keeps its size, exactly as orthographic projection should.

View code
.pe-iso {
  display: grid;
  justify-items: center;
  gap: 8px;
  width: 100%;
}
.pe-iso__scene {
  display: grid;
  place-items: center;
  width: 100%;
  height: 250px;
}
.pe-iso__grid {
  display: grid;
  grid-template-columns: repeat(2, 96px);
  gap: 14px;
  transform-style: preserve-3d;
  transform: rotateX(54.74deg) rotateZ(-45deg);
}
.pe-iso__tile {
  display: grid;
  align-content: center;
  gap: 4px;
  height: 96px;
  padding: 0 12px;
  border: 0;
  border-radius: 10px;
  text-align: left;
  font: inherit;
  cursor: pointer;
  box-shadow: -6px 6px 0 #b9c2d1;
}
.pe-iso__tile span {
  font-size: 12px;
  font-weight: 700;
}
.pe-iso__tile b {
  font: 600 22px/1 "Fraunces", Georgia, serif;
}
.pe-iso__tile--a { background: #2448a8; color: #fff; }
.pe-iso__tile--b { background: #f0b429; color: #231700; }
.pe-iso__tile--c { background: #f6f7fb; color: #1b2430; }
.pe-iso__tile--d { background: #17a37f; color: #04211a; }
.pe-iso__tile[aria-pressed="true"] {
  transform: translateZ(26px);
  box-shadow: -14px 14px 0 rgba(27, 36, 48, .22);
}
.pe-iso__tile:focus-visible {
  outline: 3px solid #1847f0;
  outline-offset: 3px;
}
.pe-iso__detail {
  min-height: 3em;
  max-width: 320px;
  margin: 0;
  color: #26303d;
  font-size: 14px;
  line-height: 1.5;
  text-align: center;
}
@media (prefers-reduced-motion: no-preference) {
  .pe-iso__tile {
    transition: transform .35s cubic-bezier(.2, .7, .2, 1), box-shadow .35s;
  }
}

Depth in interface details

Smaller touches where perspective earns its place in a real interface: a headline, a scrolling hero, keys that press in, and a device mockup.

Headline with stacked depth layers

  • translateZ layers
  • preserve-3d text
  • aria-hidden copies

Summit

Trail running series · June 2027

One real line of text plus six aria-hidden copies, each pushed 3px further back with translateZ inside a preserve-3d heading. Under perspective the copies shift and shrink toward the vanishing point, which reads as solid extruded letters, something text-shadow cannot do once the word turns. Screen readers get the word once.

View code
.pe-ext {
  display: grid;
  justify-items: center;
  gap: 18px;
  padding: 20px 0;
  perspective: 600px;
}
.pe-ext__word {
  position: relative;
  margin: 0;
  color: #ffd166;
  font: 700 clamp(60px, 17vw, 112px)/1 "Fraunces", Georgia, serif;
  letter-spacing: -.01em;
  transform-style: preserve-3d;
  transform: rotateX(18deg) rotateY(-24deg);
}
.pe-ext__face {
  position: relative;
  display: block;
}
.pe-ext__layer {
  position: absolute;
  inset: 0;
  color: #c4502f;
}
.pe-ext__layer:nth-child(2) { transform: translateZ(-3px); }
.pe-ext__layer:nth-child(3) { transform: translateZ(-6px); }
.pe-ext__layer:nth-child(4) { transform: translateZ(-9px); }
.pe-ext__layer:nth-child(5) { transform: translateZ(-12px); }
.pe-ext__layer:nth-child(6) { transform: translateZ(-15px); }
.pe-ext__layer:nth-child(7) { transform: translateZ(-18px); color: #6e2418; }
.pe-ext__sub {
  margin: 0;
  color: #e7e2d6;
  font-size: 15px;
  letter-spacing: .04em;
}
@media (prefers-reduced-motion: no-preference) {
  .pe-ext__word {
    transition: transform .8s cubic-bezier(.3, .7, .2, 1);
  }
  .pe-ext:hover .pe-ext__word {
    transform: rotateX(10deg) rotateY(22deg);
  }
}

Parallax hero built from perspective

  • perspective: 1px on a scroller
  • translateZ + scale
  • no scroll listener

Dunmore Ridge

Twelve trails, one long weekend

Plan your weekend

Pick a base camp in the valley, then choose two ridge trails a day. The longest loop is 18 km with 900 m of climbing; the shortest is a 4 km lake walk.

Shuttles leave the visitor centre every 40 minutes from 7:00. Bring layers, the ridge is often 8 degrees colder than the car park.

Scroll inside the hero, or Tab to it and use the arrow keys.

The scroll container itself holds perspective: 1px, and each layer sits further back with translateZ, then scales up by 1 plus its distance to return to its drawn size. Deeper layers then scroll more slowly with no JavaScript at all. Overflow on the scroller is fine because the layers are its direct children and nothing here needs preserve-3d. With reduced motion the layers lose their depth and scroll together. The CSS parallax examples page covers this technique in more depth.

View code
.pe-plx {
  position: relative;
  height: 360px;
  overflow-x: hidden;
  overflow-y: auto;
  background: #2a2240;
  perspective: 1px;
  perspective-origin: 50% 50%;
}
.pe-plx:focus-visible {
  outline: 3px solid #1847f0;
  outline-offset: -3px;
}
.pe-plx__layer {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 360px;
}
.pe-plx__sky {
  background: #ffd9b8;
}
.pe-plx__sun {
  position: absolute;
  top: 128px;
  right: 14%;
  width: 64px;
  height: 64px;
  border-radius: 50%;
  background: #ff8a5b;
}
img.pe-plx__layer {
  display: block;
  object-fit: cover;
  object-position: center bottom;
}
.pe-plx__copy {
  box-sizing: border-box;
  padding: 56px 24px 0;
  color: #2a2240;
  text-align: center;
}
.pe-plx__copy p {
  margin: 0;
}
.pe-plx__title {
  font: 600 clamp(34px, 7vw, 54px)/1.05 "Fraunces", Georgia, serif;
}
.pe-plx__tag {
  margin-top: 6px !important;
  font-size: 16px;
  font-weight: 600;
}
.pe-plx__content {
  position: relative;
  box-sizing: border-box;
  margin-top: 360px;
  padding: 24px clamp(18px, 5vw, 48px) 32px;
  background: #2a2240;
  color: #f1ebf7;
  font-size: 15px;
  line-height: 1.6;
}
.pe-plx__content p {
  max-width: 60ch;
  margin: 0 0 12px;
}
.pe-plx__h {
  font: 600 22px/1.2 "Fraunces", Georgia, serif;
}
@media (prefers-reduced-motion: no-preference) {
  .pe-plx__sky { transform: translateZ(-3px) scale(4); }
  .pe-plx__far { transform: translateZ(-2px) scale(3); }
  .pe-plx__copy { transform: translateZ(-1.5px) scale(2.5); }
  .pe-plx__mid { transform: translateZ(-1px) scale(2); }
  .pe-plx__near { transform: translateZ(-.4px) scale(1.4); }
}

3D keys that press in

  • rotateX side face
  • translateZ press
  • aria-pressed

Each key is a button tilted back with rotateX, with a top face lifted 14px by translateZ and a side face hinged at the bottom edge with rotateX(-90deg). Pressing lowers the top face and shrinks the side with scaleY, so the key really travels instead of faking it with a shadow. Toggle keys stay down through aria-pressed; the Leave key only dips while pressed.

View code
.pe-keys {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 14px;
  padding: 30px 0;
  perspective: 600px;
}
.pe-key {
  position: relative;
  width: 98px;
  height: 66px;
  padding: 0;
  border: 0;
  background: transparent;
  font: inherit;
  cursor: pointer;
  transform-style: preserve-3d;
  transform: rotateX(34deg);
}
.pe-key__top {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  border: 1px solid #d6cdbb;
  border-radius: 12px;
  background: #f7f3eb;
  color: #1f2328;
  font-size: 14px;
  font-weight: 700;
  transform: translateZ(14px);
}
.pe-key__side {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 14px;
  border-radius: 0 0 12px 12px;
  background: #b5a88f;
  transform-origin: center bottom;
  transform: rotateX(-90deg);
}
.pe-key[aria-pressed="true"] .pe-key__top {
  transform: translateZ(5px);
}
.pe-key[aria-pressed="true"] .pe-key__side {
  transform: rotateX(-90deg) scaleY(.36);
}
.pe-key:active .pe-key__top {
  transform: translateZ(3px);
}
.pe-key:active .pe-key__side {
  transform: rotateX(-90deg) scaleY(.22);
}
.pe-key--mute[aria-pressed="true"] .pe-key__top {
  border-color: #9e2a22;
  background: #b8322a;
  color: #fff;
}
.pe-key--mute[aria-pressed="true"] .pe-key__side,
.pe-key--leave .pe-key__side {
  background: #7d1f19;
}
.pe-key--cam[aria-pressed="true"] .pe-key__top {
  border-color: #111418;
  background: #262b33;
  color: #fff;
}
.pe-key--cam[aria-pressed="true"] .pe-key__side {
  background: #0d0f12;
}
.pe-key--leave .pe-key__top {
  border-color: #9e2a22;
  background: #b8322a;
  color: #fff;
}
.pe-key:focus-visible {
  outline: none;
}
.pe-key:focus-visible .pe-key__top {
  outline: 3px solid #1847f0;
  outline-offset: 3px;
}
@media (prefers-reduced-motion: no-preference) {
  .pe-key__top,
  .pe-key__side {
    transition: transform .12s ease-out, background-color .2s;
  }
}

Tilted phone mockup with a floating card

  • stacked translateZ edge
  • perspective: 1200px
  • transform-origin
Route photo in the app: green sea cliffs with a path along the coast

Coastline

Day 2 · 14 km left

Viewpoint ahead1.2 km, 20 min walk

A landing-page device shot made from markup, so the screenshot stays a real image you can swap. The phone body sits on three darker copies of itself pushed back with translateZ, which reads as the frame thickness, and the notification card floats 60px in front. The button straightens the phone for visitors who would rather read the screen.

View code
.pe-phone {
  display: grid;
  justify-items: center;
  gap: 10px;
  width: 100%;
}
.pe-phone__scene {
  display: grid;
  place-items: center;
  width: 100%;
  height: 400px;
  perspective: 1200px;
}
.pe-phone__device {
  position: relative;
  width: 170px;
  height: 340px;
  transform-style: preserve-3d;
  transform: rotateX(12deg) rotateY(-30deg) rotateZ(5deg);
}
.pe-phone:has([aria-pressed="true"]) .pe-phone__device {
  transform: rotateX(0deg) rotateY(0deg) rotateZ(0deg);
}
.pe-phone__edge,
.pe-phone__body {
  position: absolute;
  inset: 0;
  border-radius: 28px;
}
.pe-phone__edge {
  background: #4a4f58;
}
.pe-phone__edge--1 { transform: translateZ(-3px); }
.pe-phone__edge--2 { transform: translateZ(-6px); background: #3a3e46; }
.pe-phone__edge--3 { transform: translateZ(-9px); background: #2b2e34; }
.pe-phone__body {
  box-sizing: border-box;
  padding: 8px;
  background: #15171b;
}
.pe-phone__screen {
  position: relative;
  height: 100%;
  overflow: hidden;
  border-radius: 21px;
  background: #f6f4ef;
}
.pe-phone__screen img {
  display: block;
  width: 100%;
  height: 62%;
  object-fit: cover;
}
.pe-phone__ui {
  padding: 12px 14px;
  color: #1c2420;
}
.pe-phone__ui p {
  margin: 0;
}
.pe-phone__app {
  font: 600 20px/1.1 "Fraunces", Georgia, serif;
}
.pe-phone__trip {
  margin-top: 4px !important;
  color: #4b544f;
  font-size: 12px;
}
.pe-phone__bar {
  height: 6px;
  margin-top: 12px !important;
  border-radius: 3px;
  background: #dfe6e1;
}
.pe-phone__bar span {
  display: block;
  width: 58%;
  height: 100%;
  border-radius: 3px;
  background: #1f7a5a;
}
.pe-phone__toast {
  position: absolute;
  top: 46px;
  left: -46px;
  display: grid;
  gap: 2px;
  width: 170px;
  margin: 0;
  padding: 10px 12px;
  border-radius: 14px;
  background: #fff;
  color: #1c2420;
  font-size: 12px;
  box-shadow: 0 16px 30px rgba(15, 23, 42, .22);
  transform: translateZ(60px);
}
.pe-phone__toast b {
  font-size: 13px;
}
.pe-phone__btn {
  min-height: 44px;
  padding: 0 18px;
  border: 1.5px solid #1f7a5a;
  border-radius: 12px;
  background: #fff;
  color: #17573f;
  font: inherit;
  font-size: 14px;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
}
.pe-phone__btn[aria-pressed="true"] {
  background: #1f7a5a;
  color: #fff;
}
.pe-phone__btn:focus-visible {
  outline: 3px solid #1847f0;
  outline-offset: 3px;
}
@media (prefers-reduced-motion: no-preference) {
  .pe-phone__device {
    transition: transform .8s cubic-bezier(.3, .7, .2, 1);
  }
}

What is CSS perspective?

In plain terms, it sets the distance between the viewer and the z=0 plane. 3D-transformed elements then shrink or grow with depth instead of rendering flat.

The CSS Transforms Module Level 2 editor’s draft (W3C CSS Working Group, 2025) is explicit about the limits. Transformed elements stay flat planes with no depth, and perspective only changes how their 3D transforms project onto the screen.

The spec’s own example makes the difference easy to see. A 150px box rotated 50 degrees on the Y axis only narrows without perspective. Add perspective: 500px to its container and the near edge grows while the far edge shrinks.

The initial value is none, so nothing happens until you set one. It applies to transformable elements, it isn’t inherited, and the default vanishing point is 50% 50%, the center of the parent.

Support is solid. MDN lists the property as Baseline widely available and supported across browsers since September 2015, and CSS3 3D transforms reach 97.49% global usage with partial support counted (Can I use, checked October 2026).

Skip it and every rotateX or rotateY on a card reads as a flat squash.

How the perspective distance sets the strength of the effect

A smaller perspective value gives stronger distortion, and a larger one gives a subtler look. The W3C editor’s draft (CSS Working Group, 2025) calls 1000px a moderate amount of foreshortening and 200px an extreme amount.

Some tutorials get this backward and claim higher values strengthen the effect. MDN says it plainly. Large values cause a small transformation, small values a large one.

Valid values and units

The keyword none is the initial value and applies no perspective. Any length works for the distance, whether px, em, rem or cm. Negative values are syntax errors, so the declaration is dropped, and anything under 1px gets clamped to 1px.

Pixels stay the default for scene geometry. Whether to write the distance in px or rem follows the same logic as any other length, since rem scales with the root font size.

What happens when translateZ passes the perspective distance

An element moved along the z-axis scales by d divided by (d minus Z), where d is the perspective value and Z is the translateZ offset.

For a 500px perspective, the formula produces these scale factors.

translateZScale factorResult
-500px0.5xHalf size
0px1.0xUnchanged
250px2.0xTwice as large
400px5.0xFive times as large
Above 500pxNoneNot drawn

At exactly 500px the element reaches the viewer’s eye and its corners stretch toward infinity. Past that point it sits behind the viewer and isn’t drawn.

If an element vanishes right after you bump translateZ, you’ve probably crossed this threshold.

The same scale formula drives pure-CSS parallax scrolling, where layers pushed back with translateZ scroll more slowly than the content in front.

Is perspective: 0 a valid value?

Depends who you ask.

Older reference copies say 0 or a negative value switches perspective off. Rocket Validator flags 0 as invalid and recommends none, or a small positive length such as 1px.

The current spec and MDN’s 2026 page agree with each other. 0 parses, values under 1px render as 1px, and negative values are errors.

The spec text is the reference here. In practice none is the off switch, while 0 gives you an extreme 1px perspective that rarely suits a layout.

Choosing a value

Start from the spec’s anchors, 1000px for a restrained effect and 200px for an exaggerated one. Raise the value as the element gets wider or the rotation gets steeper. And keep translateZ under half the value unless the disappearing act is what you want.

Width matters because a rotated element pushes its edges along the z-axis. By the spec formula, a 150px box at rotateY(50deg) under perspective 500px scales its near edge to about 1.13x and its far edge to about 0.90x.

Moving the vanishing point with perspective-origin

The perspective-origin property sets where the viewer looks at the scene on the X and Y axes, and that position becomes the vanishing point. The default is 50% 50%, the center of the parent.

It accepts the keywords left, center, right, top and bottom, alone or in pairs, plus lengths and percentages measured from the top left corner of the parent’s box. Negative values work too, such as -200% 200%, which place the viewer outside the box.

On its own the property does nothing. It moves the projection that the perspective property creates, and the two are separate declarations because perspective is not a shorthand for it.

Setting top moves the vanishing point to the top edge of the container, so the scene reads as viewed from above.

Don’t mix it up with transform-origin. The perspective-origin property sits on the parent and moves the viewer for every child at once, while transform-origin sits on the child and moves the pivot for that element’s own rotation and scaling.

Shared vanishing points are the use case the spec names. Several drawings on one page keep a common perspective when they share a perspective-origin.

Perspective property or perspective() function: which one fits?

Most of the time the property on a parent is the right call, because several children usually share one scene. The function is for the case where a single element needs its own depth.

FeaturePropertyFunction
Written onThe parent elementThe transformed element, inside transform
AffectsDirect 3D-transformed childrenThat element only
Vanishing pointShared by all childrenOne per element
Origin set byperspective-origintransform-origin
Typical useCard grids, flip cards, 3D scenesOne tilted image or hover effect

The function compiles into the element’s transformation matrix. A value of perspective(500px) resolves to matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, -0.002, 0, 0, 0, 1), where -0.002 is -1/d.

Order inside the transform list matters. Put perspective() first, because MDN notes that, measured against the parent’s fixed coordinate space, transform functions apply right to left, so the projection lands after the rotation and translation.

With the property, one declaration on the container gives a card grid, or a flip card with two faces, the same vanishing point. With the function, repeating it across a grid makes every card converge on its own center, and that looks inconsistent. It’s fine for one tilted image, and it’s the only route when each element needs a separate vanishing point.

Why perspective only reaches direct children, and what preserve-3d changes

An element only picks up its parent’s perspective matrix, and a flat parent ends the chain. Setting transform-style: preserve-3d on the middle element keeps it going.

The W3C editor’s draft (CSS Working Group, 2025) describes the accumulation rule. For each element in a 3D rendering context, the parent’s perspective matrix is multiplied in, then the walk moves up one level.

Say the container holds the perspective property and its child carries a 3D transform. If that child has 3D children of its own, it needs transform-style: preserve-3d as well. The grandchild only joins the container’s scene when the child preserves 3D. Without it, the grandchild’s rotateX just shortens it inside the child’s flat plane, as the spec’s nested example shows.

Transform-style is not inherited, so MDN’s 2026 page says it must be set on every non-leaf descendant in the chain.

A fair number of property values override preserve-3d and force the used value back to flat. The usual suspects are overflow other than visible or clip, opacity below 1, filter other than none, clip-path other than none, mask-image other than none and mix-blend-mode other than normal. Clip, isolation: isolate, mask-border-source and contain: paint do the same.

Two culprits show up constantly. One is overflow: hidden on a rounded card wrapper, the other is a fade-in that starts below opacity 1. The first flattens the scene permanently, the second until the fade finishes, and neither throws a warning.

Preserve-3d also creates a stacking context and a containing block for all descendants (CSS Working Group, 2025).

The direct-parent rule has a paper trail. In November 2011 a Mozilla engineer asked on the W3C www-style list whether the spec’s word “children” meant all descendants, and argued that limiting perspective to direct parents would be simpler.

How to build a 3D card flip with CSS perspective

The spec’s own flip example is the cleanest recipe. It uses a container with perspective, a wrapper with preserve-3d, two faces with backface-visibility: hidden, and a rotateY(180deg) trigger.

  1. Give the parent container perspective. The spec example uses 500px.
  2. Build the card wrapper at a fixed size (the example uses 200px by 300px) with position: relative, transform-style: preserve-3d and transition: transform 1s.
  3. Add two faces inside it, absolutely positioned at top: 0 and left: 0, each 100% wide and 100% tall.
  4. Set backface-visibility: hidden on both faces, so the mirrored reverse side never shows through.
  5. Pre-rotate the back face with transform: rotateY(180deg), so its content reads correctly after the turn.
  6. Toggle a class on the wrapper that sets transform: rotateY(180deg). The spec flips the card on click.

Duration and easing are the part worth tuning by eye.

Put perspective on the card itself and its own rotation gets no projection, so the flip reads as a flat squash. Forget preserve-3d and the faces flatten into the wrapper, with the back face sitting on top of the front. Leave out backface-visibility and you see the mirrored front face through the back.

Ready-made variations are collected in a set of CSS flip card examples.

When CSS perspective fails or breaks the layout

Usually the property is on the wrong element. The rest comes down to side effects of a non-none value and a couple of browser bugs, and each has its own fix.

Wrong element or missing 3D transform

SymptomCauseFix
Card tilts but looks flatPerspective sits on the rotated elementMove it to the parent
Nothing moves in 3DChildren use only 2D transformsAdd rotateX, rotateY or translateZ
Span or link ignores transformsNon-replaced inline boxes are not transformableSet display to inline-block or block

The second row has a simple reason. With only 2D transforms every point sits at Z=0, where the scale factor is d divided by d, exactly 1, so perspective has nothing to project.

Stacking context and fixed-position side effects

Any perspective value other than none turns the element into a stacking context and a containing block for fixed-position descendants (MDN, 2026).

A fixed header or modal placed inside the container then anchors to that container instead of the viewport. Its z-index also competes only with elements inside the same context.

The spec’s own editors flag the requirement as unnecessary in a draft note, and suspect web compatibility keeps them from removing it.

The way out is to keep fixed elements outside the perspective container, or wrap only the 3D elements in a smaller perspective parent.

Support, prefixes and known engine bugs

Can I use splits its 3D transform support rows at Chrome 36, Firefox 16, Safari 9 and Opera 23. Tohoho Web’s reference table names the same versions as the end of vendor prefixes, so prefixed code only matters for browsers older than those.

Internet Explorer 10 and 11 count as partial support and Opera Mini has none (Can I use, checked October 2026).

Backface-visibility is the newest piece of the set. MDN’s 2026 page dates it to March 2022, when Safari 15.4 dropped the -webkit- prefix requirement, so flip cards deserve a cross-browser compatibility pass on the oldest browsers you still support.

A couple of engine bugs were still open as of October 2026.

In Firefox, a display: table child of a perspective parent gets no perspective, and putting the property on the table affects the wrong box. Mozilla Bugzilla 726601 is still NEW after 14 years, last updated in 2022. The workaround is cheap. Wrap the table in a div and transform the div.

WebKitGTK renders the effect on the first frame only and ignores scroll updates. WebKit Bugzilla 285268 was reported on 2024-12-31 and was still NEW at its latest change on 2026-09-28.

CSS Perspective FAQ

Can perspective be animated?

Yes. MDN lists the animation type as a length, so the value interpolates between two lengths like any other.

Transform-style behaves differently. It animates discretely, so preserve-3d switches on or off at once instead of easing.

How do I build a 3D cube with perspective?

Set perspective on the parent and transform-style: preserve-3d on a cube wrapper, then place six absolutely positioned faces.

MDN’s demo uses 100px faces. Each takes translateZ(50px), half the face width, after rotateY for the back, left and right faces or rotateX for the top and bottom.

Does perspective change the layout of surrounding elements?

No. Transforms change shape and position without disrupting normal document flow, according to MDN, so neighbors keep their space.

The one layout side effect is positioning, since a non-none value makes the element the containing block for fixed-position descendants.

How do I turn perspective off again?

Set perspective: none, its initial value. Inside a transform list, perspective(none) does the same job and resolves to the identity matrix.

Resetting also removes the stacking context and fixed-position containing block that any other value creates, which is often what repairs a misplaced fixed header.

The Right Order for Setting Up CSS Perspective

CSS perspective works reliably when structure comes before tuning. Declare the property on the parent first. Then set preserve-3d down the chain, and leave the distance and origin for last.

The order matters because a wrong parent or a flattened middle element produces a flat result at any value. Changing pixels first only hides the structural fault.

The trade-off is shared geometry. A parent property gives every child one vanishing point, and separate vanishing points cost one perspective() function per element.

Everything here was checked in October 2026, with the Firefox display: table bug and the WebKitGTK scroll bug both still open. Recheck this advice if either closes, and test those engines first when a scene uses tables or reacts to scrolling.