Workshop sign-ups: Saturday 9 am – 12 pm
Ages 8 to 17 · first session free
Translucent 70 × 90 px glass cards, each with an emoji at its center, placed in a circle by JavaScript (the angle is computed, so add as many cards as you like) and turning endlessly around the vertical axis, one turn every 12 s. Their back sides stay visible through the glass: a light gallery rather than a carousel of opaque tiles. A CSS 3D simulation on a flat screen, with no headset and no WebXR. 0.6 KB of JavaScript, still when the user reduces motion.
Updated
This effect is free — the 3D category contains 50 effects total, including 8 free. Effect.Labs has 840 vanilla effects. Explore the category →
Ages 8 to 17 · first session free
Drop-offs on Tuesday · 14 quai du Canal
Around the Grand-Place, then along the river
The .fx-0043-root root is 250 px tall, rgb(26, 26, 36) background, overflow: hidden. .xr-vr-gallery fills it and carries the camera (perspective: 600px). At the center, .xr-vr-gallery-inner (200 × 140 px) keeps its children in 3D (transform-style: preserve-3d) and turns from rotateY(0) to rotateY(360deg) in 12 s, linear, forever. The .xr-vr-gcard elements are all centered in the same spot (left: 50%, top: 50% and negative margins): 70 × 90 px, 8 px corners, an indigo → violet gradient at 30 and 20% opacity, a 30% indigo border, a 4 px backdrop blur, and a 1.5 rem emoji.
The script builds the circle. For each .fx-0043-root on the page, it counts the cards and gives card number i rotateY(i / count × 360deg) translateZ(80px): with five cards, one angle every 72°, 80 px from the center. Adding or removing a card in the HTML is enough, the angles follow. The radius, though, stays fixed: up to six 70 px cards, they do not touch; from seven on, they overlap (the distance between neighbors drops below 70 px). Since the back sides are not hidden, the far cards stay visible through the glass, mirrored: the whole gallery is on screen at once. Measured in Chromium 151 and Firefox 153: about 11.7% of the block's pixels change over 0.8 s, two galleries on one page each get their five cards placed, 0 errors.
The name comes from the catalog: there is no virtual reality in this code. No WebXR, no headset, no head tracking; it is a CSS 3D gallery seen on an ordinary screen, which reacts to neither mouse nor keyboard. The backdrop blur only shows when there are colors behind the cards, hence the mottled backgrounds in the examples. Playwright's WebKit flattens all CSS 3D: rendering on a real Safari was not verified.
prefers-reduced-motion: reduce, a block scoped to .fx-0043-root stops the rotation and turns the gallery by -18deg: a fixed view where the front card and its neighbors stay readable. Measured in Chromium and Firefox: 0 running animations, 0% of pixels changing over 0.8 s (11.7% without the preference)..xr-vr-gallery:hover .xr-vr-gallery-inner { animation-play-state: paused }, or a button.aria-hidden="true" on the root and the list as text next to it.CSS (3D transform, preserve-3d, perspective, backdrop-filter) + 0.6 KB of JavaScript that places the cards, no dependencies. Measured in Chromium 151 and Firefox 153: same gallery (0.96% difference on the still frame), two independent galleries, 0 errors. A pitfall measured in Chromium 151: under two nested rounded containers that clip (overflow: hidden + border-radius), part of the blurred cards is no longer drawn; Firefox is not affected. Playwright's WebKit 26.5 flattens all CSS 3D: the real Safari was not verified. No WebXR: no headset mode.
Without JavaScript, the cards stay stacked at the center and turn together like a single card. Without backdrop-filter (Chrome before 76, Firefox before 103), the cards keep their tinted glass, with no blur. If you place the gallery in a rounded card that itself sits in a rounded block, keep only one of the two rounded containers on overflow: hidden and clip the other with clip-path: inset(0 round 12px), like the frame of the examples on this page, or remove backdrop-filter from the cards.
Copy the three blocks into your page. No dependencies.
<div class="fx-0043-root">
<div class="xr-vr-gallery">
<div class="xr-vr-gallery-inner">
<div class="xr-vr-gcard">🎨</div>
<div class="xr-vr-gcard">🖼</div>
<div class="xr-vr-gcard">🎬</div>
<div class="xr-vr-gcard">🎵</div>
<div class="xr-vr-gcard">💎</div>
</div>
</div>
</div>
.fx-0043-root,
.fx-0043-root *,
.fx-0043-root *::before,
.fx-0043-root *::after {
box-sizing: border-box
}
.fx-0043-root {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 250px;
overflow: hidden;
position: relative;
background: rgb(26, 26, 36);
color: rgb(255, 255, 255);
font-family: sans-serif;
}
.xr-vr-gallery {
width: 100%;
height: 100%;
perspective: 600px;
display: flex;
align-items: center;
justify-content: center;
}
.xr-vr-gallery-inner {
position: relative;
width: 200px;
height: 140px;
transform-style: preserve-3d;
animation: xrGalleryRotate 12s linear infinite;
}
.xr-vr-gcard {
position: absolute;
width: 70px;
height: 90px;
background: linear-gradient(135deg, rgba(99, 102, 241, 0.3), rgba(139, 92, 246, 0.2));
border: 1px solid rgba(99, 102, 241, 0.3);
border-radius: 8px;
left: 50%;
top: 50%;
margin-left: -35px;
margin-top: -45px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
backdrop-filter: blur(4px);
}
@keyframes xrGalleryRotate {
from {
transform: rotateY(0deg);
}
to {
transform: rotateY(360deg);
}
}
/* Reduced motion: the gallery stops, turned slightly so the side cards stay readable. */
@media (prefers-reduced-motion: reduce) {
.fx-0043-root .xr-vr-gallery-inner { animation: none; transform: rotateY(-18deg) }
}
(function() {
// VR Card Gallery: places the cards in a circle (angle = 360° / number of cards, 80 px radius).
// One gallery per .fx-0043-root: several galleries can live on the same page.
document.querySelectorAll('.fx-0043-root').forEach(function(root) {
var gallery = root.querySelector('.xr-vr-gallery-inner');
if (!gallery) return;
var cards = gallery.querySelectorAll('.xr-vr-gcard');
var count = cards.length;
cards.forEach(function(card, i) {
var angle = (i / count) * 360;
card.style.transform = 'rotateY(' + angle + 'deg) translateZ(80px)';
});
});
})();
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Number of cards (HTML — the .xr-vr-gcard elements) |
5 | Add or remove cards: the script recomputes the angles. Beyond six, widen the radius so they do not overlap. |
Radius (JS — translateZ(80px)) |
80 px | For n 70 px cards with no overlap, the radius must be at least 35 / sin(180° / n): 60 px for 5, 92 px for 8, 114 px for 10. Widen .xr-vr-gallery-inner accordingly. |
Speed (CSS — 12s in the animation of .xr-vr-gallery-inner) |
12 s per turn | 18 to 24 s for a calm page background; 8 s for a lively showcase. Keep linear. |
Glass (CSS — background, border and backdrop-filter of .xr-vr-gcard) |
indigo → violet at 30 / 20%, 4 px blur | More opacity (50%) makes the far cards less visible; an 8 px blur gives frosted glass. The blur only acts on a colored background or an image. |
Perspective (CSS — perspective of .xr-vr-gallery) |
600px | 400 px brings the camera closer: the front card grows noticeably, the far ones shrink. 1,000 px flattens the ring. |
Card size (CSS — width, height and margins of .xr-vr-gcard) |
70 × 90 px, −35 / −45 px margins | The negative margins are half the size: for 90 × 120 px, margin-left: -45px; margin-top: -60px, and a larger radius. |
No. The name comes from the catalog, but the code is a CSS 3D gallery shown on an ordinary screen: no WebXR, no headset, no head or mouse tracking. It suggests a room you look around; for a real VR scene you need a WebXR library such as A-Frame or Three.js.
Their back sides are not hidden: through the glass of the front cards, you see the backs of the far cards, so their emoji appears reversed. That is what makes it a glass gallery. To show only the cards facing you, add backface-visibility: hidden to .xr-vr-gcard.
Yes: the script handles each .fx-0043-root separately and relies on no id. Measured with two galleries: each gets its five cards in a circle and turns on its own.