16 / 20
All 20 in 20 CSS Pagination
- 01Pure CSS Pagination with :target
- 02Pagination with Ellipsis
- 03Responsive Compact Pagination
- 04Pagination with Prev Next Icons
- 05Pagination with Jump to Page Input
- 06Pagination with Items Per Page
- 07Tailwind CSS Pagination Component
- 08Bootstrap 5 Custom Pagination
- 09Accessible ARIA Pagination
- 10Data Table Pagination Bar
- 11E-Commerce Grid Pagination
- 12Blog Archive Pagination
- 13Cursor-Based Load More Pagination
- 14Sliding Pill Active Pagination
- 15Hover Underline Pagination
- 163D Flip Perspective Pagination
- 17Vertical Side-Docked Pagination
- 18Full-Width Fluid Pagination
- 19Glassmorphism Pagination
- 20Neumorphism Soft UI Pagination
3D Flip Perspective Pagination
Each page key is a two-faced box: the number on the front, a verb on the back. Hover or focus rotates it a half turn on X and the browser swaps which face points at the camera. Perspective lives on each button rather than the row, so the outer keys do not skew, and reduced motion falls back to a plain background change.
Build UI like this from scratch: Flexbox, Grid, animations & responsive CSS
ZTM · The CSS Bootcamp · 30‑day money-back guarantee · Affiliate link
The code
<section class="pgn-16" aria-label="3D Flip / Depth Perspective Pagination demo">
<div class="pgn-16__stage">
<header class="pgn-16__head">
<p class="pgn-16__eyebrow">transform-style: preserve-3d</p>
<h2 class="pgn-16__title">Each key flips to reveal its face</h2>
<p class="pgn-16__sub">Hover or focus a number. Two faces sit on the same box, one rotated a quarter turn on X; the parent rotates and the browser swaps which face points at the camera.</p>
</header>
<nav class="pgn-16__nav" aria-label="Pagination">
<ol class="pgn-16__list">
<li><button class="pgn-16__key" type="button"><span class="pgn-16__inner"><span class="pgn-16__face">1</span><span class="pgn-16__face pgn-16__face--back">Go</span></span></button></li>
<li><button class="pgn-16__key" type="button"><span class="pgn-16__inner"><span class="pgn-16__face">2</span><span class="pgn-16__face pgn-16__face--back">Go</span></span></button></li>
<li><button class="pgn-16__key" type="button" aria-current="page"><span class="pgn-16__inner"><span class="pgn-16__face">3</span><span class="pgn-16__face pgn-16__face--back">Here</span></span></button></li>
<li><button class="pgn-16__key" type="button"><span class="pgn-16__inner"><span class="pgn-16__face">4</span><span class="pgn-16__face pgn-16__face--back">Go</span></span></button></li>
<li><button class="pgn-16__key" type="button"><span class="pgn-16__inner"><span class="pgn-16__face">5</span><span class="pgn-16__face pgn-16__face--back">Go</span></span></button></li>
</ol>
</nav>
<p class="pgn-16__note">Page 3 of 5 · 60 results</p>
</div>
</section><section class="pgn-16" aria-label="3D Flip / Depth Perspective Pagination demo">
<div class="pgn-16__stage">
<header class="pgn-16__head">
<p class="pgn-16__eyebrow">transform-style: preserve-3d</p>
<h2 class="pgn-16__title">Each key flips to reveal its face</h2>
<p class="pgn-16__sub">Hover or focus a number. Two faces sit on the same box, one rotated a quarter turn on X; the parent rotates and the browser swaps which face points at the camera.</p>
</header>
<nav class="pgn-16__nav" aria-label="Pagination">
<ol class="pgn-16__list">
<li><button class="pgn-16__key" type="button"><span class="pgn-16__inner"><span class="pgn-16__face">1</span><span class="pgn-16__face pgn-16__face--back">Go</span></span></button></li>
<li><button class="pgn-16__key" type="button"><span class="pgn-16__inner"><span class="pgn-16__face">2</span><span class="pgn-16__face pgn-16__face--back">Go</span></span></button></li>
<li><button class="pgn-16__key" type="button" aria-current="page"><span class="pgn-16__inner"><span class="pgn-16__face">3</span><span class="pgn-16__face pgn-16__face--back">Here</span></span></button></li>
<li><button class="pgn-16__key" type="button"><span class="pgn-16__inner"><span class="pgn-16__face">4</span><span class="pgn-16__face pgn-16__face--back">Go</span></span></button></li>
<li><button class="pgn-16__key" type="button"><span class="pgn-16__inner"><span class="pgn-16__face">5</span><span class="pgn-16__face pgn-16__face--back">Go</span></span></button></li>
</ol>
</nav>
<p class="pgn-16__note">Page 3 of 5 · 60 results</p>
</div>
</section>.pgn-16 {
--bg: #0c0c0f;
--surface: #17171c;
--surface-2: #212129;
--text: #f4f4f5;
--muted: #8f8f99;
--line: #2b2b34;
--acc: #a78bfa;
--acc-fg: #12101b;
--ring: #c4b5fd;
}
@supports (color: oklch(50% 0 0)) {
.pgn-16 {
--bg: oklch(14% .008 285);
--surface: oklch(19% .01 285);
--surface-2: oklch(24% .014 285);
--text: oklch(96% .002 286);
--muted: oklch(66% .014 286);
--line: oklch(29% .012 285);
--acc: oklch(75% .14 300);
--acc-fg: oklch(16% .03 300);
--ring: oklch(82% .1 300);
}
}
@media (prefers-color-scheme: light) {
.pgn-16:not([data-theme="dark"]) {
--bg: #f6f5fa;
--surface: #ffffff;
--surface-2: #efeef5;
--text: #12111a;
--muted: #6b6a7a;
--line: #e3e1ec;
--acc: #6d28d9;
--acc-fg: #ffffff;
--ring: #7c3aed;
}
}
.pgn-16[data-theme="light"] {
--bg: #f6f5fa;
--surface: #ffffff;
--surface-2: #efeef5;
--text: #12111a;
--muted: #6b6a7a;
--line: #e3e1ec;
--acc: #6d28d9;
--acc-fg: #ffffff;
--ring: #7c3aed;
}
.pgn-16 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
padding: clamp(1.5rem,5vw,4rem);
background: var(--bg);
color: var(--text);
font-family: system-ui,-apple-system,"Segoe UI",sans-serif;
-webkit-font-smoothing: antialiased;
}
.pgn-16,
.pgn-16 *,
.pgn-16 *::before,
.pgn-16 *::after {
box-sizing: border-box;
}
.pgn-16__stage {
width: 100%;
max-width: 44rem;
display: grid;
gap: 2.25rem;
justify-items: center;
text-align: center;
}
.pgn-16__eyebrow {
margin: 0 0 .4rem;
font-family: ui-monospace,SFMono-Regular,Menlo,monospace;
font-size: .6875rem;
letter-spacing: .06em;
color: var(--muted);
}
.pgn-16__title {
margin: 0 0 .55rem;
font-size: clamp(1.4rem,3.2vw,1.9rem);
font-weight: 600;
letter-spacing: -.025em;
text-wrap: balance;
}
.pgn-16__sub {
margin: 0;
max-width: 33rem;
font-size: .875rem;
line-height: 1.6;
color: var(--muted);
text-wrap: pretty;
}
.pgn-16__nav {
perspective: 600px;
}
.pgn-16__list {
display: flex;
gap: .6rem;
list-style: none;
margin: 0;
padding: 0;
}
.pgn-16__key {
width: 3.5rem;
height: 3.5rem;
padding: 0;
border: 0;
background: transparent;
font: inherit;
cursor: pointer;
perspective: 600px;
}
.pgn-16__key:focus-visible {
outline: 2px solid var(--ring);
outline-offset: 4px;
border-radius: .75rem;
}
.pgn-16__inner {
position: relative;
display: block;
width: 100%;
height: 100%;
transform-style: preserve-3d;
transition: rotate .42s cubic-bezier(.34,1.2,.5,1);
}
.pgn-16__key:hover .pgn-16__inner,
.pgn-16__key:focus-visible .pgn-16__inner {
rotate: x -180deg;
}
.pgn-16__face {
position: absolute;
inset: 0;
display: grid;
place-items: center;
border: 1px solid var(--line);
border-radius: .75rem;
background: var(--surface);
color: var(--text);
font-size: 1.0625rem;
font-variant-numeric: tabular-nums;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
box-shadow: 0 1px 0 color-mix(in oklab,var(--text) 8%,transparent) inset;
}
.pgn-16__face--back {
rotate: x 180deg;
background: var(--acc);
color: var(--acc-fg);
border-color: transparent;
font-size: .8125rem;
font-weight: 600;
letter-spacing: .01em;
}
.pgn-16__key[aria-current="page"] .pgn-16__face {
background: var(--surface-2);
border-color: color-mix(in oklab,var(--acc) 55%,var(--line));
font-weight: 600;
}
.pgn-16__note {
margin: 0;
font-size: .8125rem;
color: var(--muted);
font-variant-numeric: tabular-nums;
}
@media (prefers-reduced-motion: reduce) {
.pgn-16 * {
animation: none !important;
transition: none !important;
}
.pgn-16__key:hover .pgn-16__inner,
.pgn-16__key:focus-visible .pgn-16__inner {
rotate: none;
}
.pgn-16__key:hover .pgn-16__face,
.pgn-16__key:focus-visible .pgn-16__face {
background: var(--surface-2);
}
}.pgn-16 {
--bg: #0c0c0f;
--surface: #17171c;
--surface-2: #212129;
--text: #f4f4f5;
--muted: #8f8f99;
--line: #2b2b34;
--acc: #a78bfa;
--acc-fg: #12101b;
--ring: #c4b5fd;
}
@supports (color: oklch(50% 0 0)) {
.pgn-16 {
--bg: oklch(14% .008 285);
--surface: oklch(19% .01 285);
--surface-2: oklch(24% .014 285);
--text: oklch(96% .002 286);
--muted: oklch(66% .014 286);
--line: oklch(29% .012 285);
--acc: oklch(75% .14 300);
--acc-fg: oklch(16% .03 300);
--ring: oklch(82% .1 300);
}
}
@media (prefers-color-scheme: light) {
.pgn-16:not([data-theme="dark"]) {
--bg: #f6f5fa;
--surface: #ffffff;
--surface-2: #efeef5;
--text: #12111a;
--muted: #6b6a7a;
--line: #e3e1ec;
--acc: #6d28d9;
--acc-fg: #ffffff;
--ring: #7c3aed;
}
}
.pgn-16[data-theme="light"] {
--bg: #f6f5fa;
--surface: #ffffff;
--surface-2: #efeef5;
--text: #12111a;
--muted: #6b6a7a;
--line: #e3e1ec;
--acc: #6d28d9;
--acc-fg: #ffffff;
--ring: #7c3aed;
}
.pgn-16 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
padding: clamp(1.5rem,5vw,4rem);
background: var(--bg);
color: var(--text);
font-family: system-ui,-apple-system,"Segoe UI",sans-serif;
-webkit-font-smoothing: antialiased;
}
.pgn-16,
.pgn-16 *,
.pgn-16 *::before,
.pgn-16 *::after {
box-sizing: border-box;
}
.pgn-16__stage {
width: 100%;
max-width: 44rem;
display: grid;
gap: 2.25rem;
justify-items: center;
text-align: center;
}
.pgn-16__eyebrow {
margin: 0 0 .4rem;
font-family: ui-monospace,SFMono-Regular,Menlo,monospace;
font-size: .6875rem;
letter-spacing: .06em;
color: var(--muted);
}
.pgn-16__title {
margin: 0 0 .55rem;
font-size: clamp(1.4rem,3.2vw,1.9rem);
font-weight: 600;
letter-spacing: -.025em;
text-wrap: balance;
}
.pgn-16__sub {
margin: 0;
max-width: 33rem;
font-size: .875rem;
line-height: 1.6;
color: var(--muted);
text-wrap: pretty;
}
.pgn-16__nav {
perspective: 600px;
}
.pgn-16__list {
display: flex;
gap: .6rem;
list-style: none;
margin: 0;
padding: 0;
}
.pgn-16__key {
width: 3.5rem;
height: 3.5rem;
padding: 0;
border: 0;
background: transparent;
font: inherit;
cursor: pointer;
perspective: 600px;
}
.pgn-16__key:focus-visible {
outline: 2px solid var(--ring);
outline-offset: 4px;
border-radius: .75rem;
}
.pgn-16__inner {
position: relative;
display: block;
width: 100%;
height: 100%;
transform-style: preserve-3d;
transition: rotate .42s cubic-bezier(.34,1.2,.5,1);
}
.pgn-16__key:hover .pgn-16__inner,
.pgn-16__key:focus-visible .pgn-16__inner {
rotate: x -180deg;
}
.pgn-16__face {
position: absolute;
inset: 0;
display: grid;
place-items: center;
border: 1px solid var(--line);
border-radius: .75rem;
background: var(--surface);
color: var(--text);
font-size: 1.0625rem;
font-variant-numeric: tabular-nums;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
box-shadow: 0 1px 0 color-mix(in oklab,var(--text) 8%,transparent) inset;
}
.pgn-16__face--back {
rotate: x 180deg;
background: var(--acc);
color: var(--acc-fg);
border-color: transparent;
font-size: .8125rem;
font-weight: 600;
letter-spacing: .01em;
}
.pgn-16__key[aria-current="page"] .pgn-16__face {
background: var(--surface-2);
border-color: color-mix(in oklab,var(--acc) 55%,var(--line));
font-weight: 600;
}
.pgn-16__note {
margin: 0;
font-size: .8125rem;
color: var(--muted);
font-variant-numeric: tabular-nums;
}
@media (prefers-reduced-motion: reduce) {
.pgn-16 * {
animation: none !important;
transition: none !important;
}
.pgn-16__key:hover .pgn-16__inner,
.pgn-16__key:focus-visible .pgn-16__inner {
rotate: none;
}
.pgn-16__key:hover .pgn-16__face,
.pgn-16__key:focus-visible .pgn-16__face {
background: var(--surface-2);
}
}Here's a working CSS Pagination from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: 3D Flip Perspective Pagination
Source: https://codefronts.com/navigation/css-pagination/3d-flip-perspective-pagination/
Each page key is a two-faced box: the number on the front, a verb on the back. Hover or focus rotates it a half turn on X and the browser swaps which face points at the camera. Perspective lives on each button rather than the row, so the outer keys do not skew, and reduced motion falls back to a plain background change.
## HTML
```html
<section class="pgn-16" aria-label="3D Flip / Depth Perspective Pagination demo">
<div class="pgn-16__stage">
<header class="pgn-16__head">
<p class="pgn-16__eyebrow">transform-style: preserve-3d</p>
<h2 class="pgn-16__title">Each key flips to reveal its face</h2>
<p class="pgn-16__sub">Hover or focus a number. Two faces sit on the same box, one rotated a quarter turn on X; the parent rotates and the browser swaps which face points at the camera.</p>
</header>
<nav class="pgn-16__nav" aria-label="Pagination">
<ol class="pgn-16__list">
<li><button class="pgn-16__key" type="button"><span class="pgn-16__inner"><span class="pgn-16__face">1</span><span class="pgn-16__face pgn-16__face--back">Go</span></span></button></li>
<li><button class="pgn-16__key" type="button"><span class="pgn-16__inner"><span class="pgn-16__face">2</span><span class="pgn-16__face pgn-16__face--back">Go</span></span></button></li>
<li><button class="pgn-16__key" type="button" aria-current="page"><span class="pgn-16__inner"><span class="pgn-16__face">3</span><span class="pgn-16__face pgn-16__face--back">Here</span></span></button></li>
<li><button class="pgn-16__key" type="button"><span class="pgn-16__inner"><span class="pgn-16__face">4</span><span class="pgn-16__face pgn-16__face--back">Go</span></span></button></li>
<li><button class="pgn-16__key" type="button"><span class="pgn-16__inner"><span class="pgn-16__face">5</span><span class="pgn-16__face pgn-16__face--back">Go</span></span></button></li>
</ol>
</nav>
<p class="pgn-16__note">Page 3 of 5 · 60 results</p>
</div>
</section>
```
## CSS
```css
.pgn-16 {
--bg: #0c0c0f;
--surface: #17171c;
--surface-2: #212129;
--text: #f4f4f5;
--muted: #8f8f99;
--line: #2b2b34;
--acc: #a78bfa;
--acc-fg: #12101b;
--ring: #c4b5fd;
}
@supports (color: oklch(50% 0 0)) {
.pgn-16 {
--bg: oklch(14% .008 285);
--surface: oklch(19% .01 285);
--surface-2: oklch(24% .014 285);
--text: oklch(96% .002 286);
--muted: oklch(66% .014 286);
--line: oklch(29% .012 285);
--acc: oklch(75% .14 300);
--acc-fg: oklch(16% .03 300);
--ring: oklch(82% .1 300);
}
}
@media (prefers-color-scheme: light) {
.pgn-16:not([data-theme="dark"]) {
--bg: #f6f5fa;
--surface: #ffffff;
--surface-2: #efeef5;
--text: #12111a;
--muted: #6b6a7a;
--line: #e3e1ec;
--acc: #6d28d9;
--acc-fg: #ffffff;
--ring: #7c3aed;
}
}
.pgn-16[data-theme="light"] {
--bg: #f6f5fa;
--surface: #ffffff;
--surface-2: #efeef5;
--text: #12111a;
--muted: #6b6a7a;
--line: #e3e1ec;
--acc: #6d28d9;
--acc-fg: #ffffff;
--ring: #7c3aed;
}
.pgn-16 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
padding: clamp(1.5rem,5vw,4rem);
background: var(--bg);
color: var(--text);
font-family: system-ui,-apple-system,"Segoe UI",sans-serif;
-webkit-font-smoothing: antialiased;
}
.pgn-16,
.pgn-16 *,
.pgn-16 *::before,
.pgn-16 *::after {
box-sizing: border-box;
}
.pgn-16__stage {
width: 100%;
max-width: 44rem;
display: grid;
gap: 2.25rem;
justify-items: center;
text-align: center;
}
.pgn-16__eyebrow {
margin: 0 0 .4rem;
font-family: ui-monospace,SFMono-Regular,Menlo,monospace;
font-size: .6875rem;
letter-spacing: .06em;
color: var(--muted);
}
.pgn-16__title {
margin: 0 0 .55rem;
font-size: clamp(1.4rem,3.2vw,1.9rem);
font-weight: 600;
letter-spacing: -.025em;
text-wrap: balance;
}
.pgn-16__sub {
margin: 0;
max-width: 33rem;
font-size: .875rem;
line-height: 1.6;
color: var(--muted);
text-wrap: pretty;
}
.pgn-16__nav {
perspective: 600px;
}
.pgn-16__list {
display: flex;
gap: .6rem;
list-style: none;
margin: 0;
padding: 0;
}
.pgn-16__key {
width: 3.5rem;
height: 3.5rem;
padding: 0;
border: 0;
background: transparent;
font: inherit;
cursor: pointer;
perspective: 600px;
}
.pgn-16__key:focus-visible {
outline: 2px solid var(--ring);
outline-offset: 4px;
border-radius: .75rem;
}
.pgn-16__inner {
position: relative;
display: block;
width: 100%;
height: 100%;
transform-style: preserve-3d;
transition: rotate .42s cubic-bezier(.34,1.2,.5,1);
}
.pgn-16__key:hover .pgn-16__inner,
.pgn-16__key:focus-visible .pgn-16__inner {
rotate: x -180deg;
}
.pgn-16__face {
position: absolute;
inset: 0;
display: grid;
place-items: center;
border: 1px solid var(--line);
border-radius: .75rem;
background: var(--surface);
color: var(--text);
font-size: 1.0625rem;
font-variant-numeric: tabular-nums;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
box-shadow: 0 1px 0 color-mix(in oklab,var(--text) 8%,transparent) inset;
}
.pgn-16__face--back {
rotate: x 180deg;
background: var(--acc);
color: var(--acc-fg);
border-color: transparent;
font-size: .8125rem;
font-weight: 600;
letter-spacing: .01em;
}
.pgn-16__key[aria-current="page"] .pgn-16__face {
background: var(--surface-2);
border-color: color-mix(in oklab,var(--acc) 55%,var(--line));
font-weight: 600;
}
.pgn-16__note {
margin: 0;
font-size: .8125rem;
color: var(--muted);
font-variant-numeric: tabular-nums;
}
@media (prefers-reduced-motion: reduce) {
.pgn-16 * {
animation: none !important;
transition: none !important;
}
.pgn-16__key:hover .pgn-16__inner,
.pgn-16__key:focus-visible .pgn-16__inner {
rotate: none;
}
.pgn-16__key:hover .pgn-16__face,
.pgn-16__key:focus-visible .pgn-16__face {
background: var(--surface-2);
}
}
```Here's a working CSS Pagination from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: 3D Flip Perspective Pagination
Source: https://codefronts.com/navigation/css-pagination/3d-flip-perspective-pagination/
Each page key is a two-faced box: the number on the front, a verb on the back. Hover or focus rotates it a half turn on X and the browser swaps which face points at the camera. Perspective lives on each button rather than the row, so the outer keys do not skew, and reduced motion falls back to a plain background change.
## HTML
```html
<section class="pgn-16" aria-label="3D Flip / Depth Perspective Pagination demo">
<div class="pgn-16__stage">
<header class="pgn-16__head">
<p class="pgn-16__eyebrow">transform-style: preserve-3d</p>
<h2 class="pgn-16__title">Each key flips to reveal its face</h2>
<p class="pgn-16__sub">Hover or focus a number. Two faces sit on the same box, one rotated a quarter turn on X; the parent rotates and the browser swaps which face points at the camera.</p>
</header>
<nav class="pgn-16__nav" aria-label="Pagination">
<ol class="pgn-16__list">
<li><button class="pgn-16__key" type="button"><span class="pgn-16__inner"><span class="pgn-16__face">1</span><span class="pgn-16__face pgn-16__face--back">Go</span></span></button></li>
<li><button class="pgn-16__key" type="button"><span class="pgn-16__inner"><span class="pgn-16__face">2</span><span class="pgn-16__face pgn-16__face--back">Go</span></span></button></li>
<li><button class="pgn-16__key" type="button" aria-current="page"><span class="pgn-16__inner"><span class="pgn-16__face">3</span><span class="pgn-16__face pgn-16__face--back">Here</span></span></button></li>
<li><button class="pgn-16__key" type="button"><span class="pgn-16__inner"><span class="pgn-16__face">4</span><span class="pgn-16__face pgn-16__face--back">Go</span></span></button></li>
<li><button class="pgn-16__key" type="button"><span class="pgn-16__inner"><span class="pgn-16__face">5</span><span class="pgn-16__face pgn-16__face--back">Go</span></span></button></li>
</ol>
</nav>
<p class="pgn-16__note">Page 3 of 5 · 60 results</p>
</div>
</section>
```
## CSS
```css
.pgn-16 {
--bg: #0c0c0f;
--surface: #17171c;
--surface-2: #212129;
--text: #f4f4f5;
--muted: #8f8f99;
--line: #2b2b34;
--acc: #a78bfa;
--acc-fg: #12101b;
--ring: #c4b5fd;
}
@supports (color: oklch(50% 0 0)) {
.pgn-16 {
--bg: oklch(14% .008 285);
--surface: oklch(19% .01 285);
--surface-2: oklch(24% .014 285);
--text: oklch(96% .002 286);
--muted: oklch(66% .014 286);
--line: oklch(29% .012 285);
--acc: oklch(75% .14 300);
--acc-fg: oklch(16% .03 300);
--ring: oklch(82% .1 300);
}
}
@media (prefers-color-scheme: light) {
.pgn-16:not([data-theme="dark"]) {
--bg: #f6f5fa;
--surface: #ffffff;
--surface-2: #efeef5;
--text: #12111a;
--muted: #6b6a7a;
--line: #e3e1ec;
--acc: #6d28d9;
--acc-fg: #ffffff;
--ring: #7c3aed;
}
}
.pgn-16[data-theme="light"] {
--bg: #f6f5fa;
--surface: #ffffff;
--surface-2: #efeef5;
--text: #12111a;
--muted: #6b6a7a;
--line: #e3e1ec;
--acc: #6d28d9;
--acc-fg: #ffffff;
--ring: #7c3aed;
}
.pgn-16 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
padding: clamp(1.5rem,5vw,4rem);
background: var(--bg);
color: var(--text);
font-family: system-ui,-apple-system,"Segoe UI",sans-serif;
-webkit-font-smoothing: antialiased;
}
.pgn-16,
.pgn-16 *,
.pgn-16 *::before,
.pgn-16 *::after {
box-sizing: border-box;
}
.pgn-16__stage {
width: 100%;
max-width: 44rem;
display: grid;
gap: 2.25rem;
justify-items: center;
text-align: center;
}
.pgn-16__eyebrow {
margin: 0 0 .4rem;
font-family: ui-monospace,SFMono-Regular,Menlo,monospace;
font-size: .6875rem;
letter-spacing: .06em;
color: var(--muted);
}
.pgn-16__title {
margin: 0 0 .55rem;
font-size: clamp(1.4rem,3.2vw,1.9rem);
font-weight: 600;
letter-spacing: -.025em;
text-wrap: balance;
}
.pgn-16__sub {
margin: 0;
max-width: 33rem;
font-size: .875rem;
line-height: 1.6;
color: var(--muted);
text-wrap: pretty;
}
.pgn-16__nav {
perspective: 600px;
}
.pgn-16__list {
display: flex;
gap: .6rem;
list-style: none;
margin: 0;
padding: 0;
}
.pgn-16__key {
width: 3.5rem;
height: 3.5rem;
padding: 0;
border: 0;
background: transparent;
font: inherit;
cursor: pointer;
perspective: 600px;
}
.pgn-16__key:focus-visible {
outline: 2px solid var(--ring);
outline-offset: 4px;
border-radius: .75rem;
}
.pgn-16__inner {
position: relative;
display: block;
width: 100%;
height: 100%;
transform-style: preserve-3d;
transition: rotate .42s cubic-bezier(.34,1.2,.5,1);
}
.pgn-16__key:hover .pgn-16__inner,
.pgn-16__key:focus-visible .pgn-16__inner {
rotate: x -180deg;
}
.pgn-16__face {
position: absolute;
inset: 0;
display: grid;
place-items: center;
border: 1px solid var(--line);
border-radius: .75rem;
background: var(--surface);
color: var(--text);
font-size: 1.0625rem;
font-variant-numeric: tabular-nums;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
box-shadow: 0 1px 0 color-mix(in oklab,var(--text) 8%,transparent) inset;
}
.pgn-16__face--back {
rotate: x 180deg;
background: var(--acc);
color: var(--acc-fg);
border-color: transparent;
font-size: .8125rem;
font-weight: 600;
letter-spacing: .01em;
}
.pgn-16__key[aria-current="page"] .pgn-16__face {
background: var(--surface-2);
border-color: color-mix(in oklab,var(--acc) 55%,var(--line));
font-weight: 600;
}
.pgn-16__note {
margin: 0;
font-size: .8125rem;
color: var(--muted);
font-variant-numeric: tabular-nums;
}
@media (prefers-reduced-motion: reduce) {
.pgn-16 * {
animation: none !important;
transition: none !important;
}
.pgn-16__key:hover .pgn-16__inner,
.pgn-16__key:focus-visible .pgn-16__inner {
rotate: none;
}
.pgn-16__key:hover .pgn-16__face,
.pgn-16__key:focus-visible .pgn-16__face {
background: var(--surface-2);
}
}
```How this works
Two absolutely positioned faces share one box inside a wrapper that keeps its children in 3D space:
.pgn-16__inner { transform-style: preserve-3d; transition: rotate .42s …; }
.pgn-16__face { backface-visibility: hidden; }
.pgn-16__face--back { rotate: x 180deg; }
.pgn-16__key:hover .pgn-16__inner { rotate: x -180deg; }Because both faces hide their backs, only the one facing the camera paints. Rotation uses the independent rotate property rather than the transform shorthand, so it stays separately animatable from any other transform on the element.
perspective: 600px sits on the button, giving every key its own vanishing point.
Make it yours
- Switch to
rotate: yfor a horizontal card flip, or a small angle like 12deg for a tilt instead of a full turn. perspectiveis the depth dial: 600px is pronounced, 1200px subtle. Move it to the<ol>and the outer keys splay like a fan.- The back face is a good place for a verb — Go, Open, Page 4 — or a small arrow icon.
- Key size is a single width/height pair; increase the cubic-bezier overshoot for a snappier keypress.
Gotchas — read before shipping
preserve-3dis destroyed byoverflow: hidden,filter,opacity < 1orclip-pathanywhere on the chain — the faces flatten and both show at once.- Safari still needs
-webkit-backface-visibilityalongside the standard property. - Text on a rotating element can render blurry mid-transition; whole-pixel face sizes help.
- Never let the back face carry meaning — it is unreachable for a screen reader. The accessible name lives on the button.
- Putting
perspectiveon the list instead of the button makes the keys inconsistent across the row.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 114+ | 17.5+ | 121+ | 114+ |
Floor set by oklch(), color-mix(), text-wrap as this demo is written. The core technique itself goes back further — Chrome 104+.
The floor is the individual rotate property (Chrome 104, Safari 14.1, Firefox 72) plus preserve-3d, which is universal. Safari needs -webkit-backface-visibility. Reduced motion swaps the flip for a background change.