Ring carousel
A carousel in the round. The cards stand on a ring seen from a little above, the one in front at full ink and the others veiled according to their depth.
Documentation
-
Copy to Webflow
Add Geist and Geist Mono to your site's fonts (Site settings, Fonts, Google Fonts) before pasting: Webflow removes any font your site has not installed. -
Add custom CSS
Custom CSS in WebflowThis CSS already ships inside the Webflow embed, so a pasted section needs nothing added. Paste it yourself, in the page or the site custom code, only if the effect runs on several pages. The tutorial explains where.
See the tutorialGive the cards a width: the whole ring is built from it. Leave air above the stage, where the far cards rise, and put no overflow, opacity or filter on the ring box, which would flatten it: clip on the section instead. -
Add custom JS
Custom JavaScript in WebflowThis script already ships inside the Webflow embed, settings block included. You paste it yourself, before the closing body tag of the page or the site custom code, only when the effect has to run on several pages, and the tutorial says how to choose.
See the tutorial -
Tune it
How to add a custom attribute in WebflowA custom attribute is a pair, a name and a value, typed in the Settings panel of the element you selected, under Custom attributes. The name is what the script looks for, the value is what it reads. The tutorial shows the panel.
See the tutorialCustom attributesSelect the section on the canvas and open the Settings panel. Under Custom attributes, add a pair: the name on the left, the value on the right.Custom attribute What it does data-ring-carousel Marks the section that holds the ring. It takes no value: leave the value field empty. That section is what the engine mounts on, what carries the settings and the states, and what it looks for the ring, the counter, the caption line and the buttons in. Without it on any section of the page, the embed marks the section holding each ring (data-ring-carousel-ring). data-ring-carousel-ring Marks the box that holds the cards, the one the engine turns. Without it the parent of the first marked card is taken; with neither, nothing is mounted and the console says so. [ Required ] data-ring-carousel-card Marks one card, a child of the ring. Three at least. Without it on any of them the engine takes the ring's own children as the cards. [ Optional ] data-ring-carousel-stage Marks the box the hand grabs, which also takes the wheel and the keyboard focus, and which the engine gives its perspective. Without it the parent of the ring is taken. [ Optional ] data-ring-carousel-caption Marks the caption of a card, inside it. With the script it is hidden in the card and shown in the display line when the card is in front. Without it the alt of the card's picture is used. [ Optional ] data-ring-carousel-display Marks the line that shows the caption of the card in front, which comes out of a mask at each change. Without it no caption is shown. [ Optional ] data-ring-carousel-current Marks the number of the card in front. The engine fills it with a reel that rolls as the ring turns. Without it there is no current number. [ Optional ] data-ring-carousel-total Marks the total of the counter, which the engine writes as the card count. Without it there is no total. [ Optional ] data-ring-carousel-prev Marks a button that steps the ring one card back. Without it the drag, the wheel and the arrow keys still do. [ Optional ] data-ring-carousel-next Marks a button that steps the ring one card further. Without it the drag, the wheel and the arrow keys still do. [ Optional ] data-ring-carousel-state Written by the engine on the section: ready, or still under reduced motion. Never written by hand; the stylesheet reads it to pile the cards on the ring and show the hand. [ Optional ] data-ring-carousel-dragging Written by the engine on the section while a drag runs. Never written by hand; the stylesheet reads it for the closed hand. [ Optional ] data-ring-carousel-active Written by the engine on the card in front. Never written by hand; a rule of the page may read it. [ Optional ] SettingsEvery setting is a custom attribute too, added the same way and on the same section. An attribute always wins over the defaults written in the code.Attribute Default What it is data-ring-carousel-color #15100c Colour of the current number of the counter and of the caption, the ink in the cream and ink look. Options: any CSS colour; without the setting, --ring-carousel-color on the section takes over. data-ring-carousel-veil #15100c Colour of the veil laid over the cards according to their depth. Options: any CSS colour; the ink darkens the far cards, the colour of the page fades them into it; without the setting, --ring-carousel-veil on the section takes over. data-ring-carousel-dim 0.45 Opacity of the veil on the card at the very back, 0 to 1; a card on the side takes about a quarter of it. data-ring-carousel-tilt 10 How high the camera stands over the ring, in degrees; the far cards rise behind the one in front. data-ring-carousel-depth 5 Distance of the camera, in card widths. data-ring-carousel-gap 0.8 Space left between two cards along the ring, as a share of the card width; with the width and the number of cards it gives the radius. data-ring-carousel-friction 0.965 Share of its speed a spinning ring keeps from one frame to the next, at 60 frames a second. data-ring-carousel-snap 140 Stiffness of the spring that docks a card and that steps the ring from the keys, the buttons and a click. data-ring-carousel-autoplay 0 Time between two cards when the ring turns by itself, in seconds.
Two files and their pictures (8), no build step.
It runs in the browser only. Load it with a script tag in plain HTML, in a
client-side script in Astro, never in the frontmatter. In React, call
RingCarousel.mount(ref.current) in useEffect and
destroy() in its cleanup.
-
Tune it, either way
Per element with an attribute, or once for the whole site by editingDEFAULTSat the top of the file.Attribute Default What it is Lower Higher data-ring-carousel-color #15100c Colour of the current number of the counter and of the caption, the ink in the cream and ink look. Options: any CSS colour; without the setting, --ring-carousel-color on the section takes over. data-ring-carousel-veil #15100c Colour of the veil laid over the cards according to their depth. Options: any CSS colour; the ink darkens the far cards, the colour of the page fades them into it; without the setting, --ring-carousel-veil on the section takes over. data-ring-carousel-dim 0.45 Opacity of the veil on the card at the very back, 0 to 1; a card on the side takes about a quarter of it. At 0 no veil, every card at full ink and less depth to read Past 0.7 the far cards are flat slabs of the veil colour data-ring-carousel-tilt 10 How high the camera stands over the ring, in degrees; the far cards rise behind the one in front. At 0 the ring is seen level, the far cards hidden behind the near ones Past 20 the ring is seen from well above and the cards lean hard towards the viewer data-ring-carousel-depth 5 Distance of the camera, in card widths. Under 3 a close camera: a strong perspective, far cards much smaller Past 8 a far camera: the ring flattens and every card is nearly the same size data-ring-carousel-gap 0.8 Space left between two cards along the ring, as a share of the card width; with the width and the number of cards it gives the radius. At 0 the cards touch edge to edge, a tight drum A wide, airy ring, which takes more of the page width data-ring-carousel-friction 0.965 Share of its speed a spinning ring keeps from one frame to the next, at 60 frames a second. A heavy ring that stops soon after the hand lets go A light ring that spins on for several cards data-ring-carousel-snap 140 Stiffness of the spring that docks a card and that steps the ring from the keys, the buttons and a click. A lazy, soft dock A quick, firm dock; past 400 it reads as a click data-ring-carousel-autoplay 0 Time between two cards when the ring turns by itself, in seconds. At 0 the ring never turns by itself A slower slideshow -
Drive it from JavaScript
When a button of your own has to turn the ring, when the cards change size after load, or when the page reacts to the card in front.const element = document.querySelector('[data-ring-carousel]'); element.addEventListener('ringcarousel:change', (e) => { console.log(e.detail); }); const ring = RingCarousel.mount(element); // the section auto() mounted, or a new one ring.select(3); // throws the ring so that it docks on the fourth card ring.next(); // one card further, on the spring element.addEventListener('ringcarousel:change', (e) => { console.log(e.detail.index); // the card in front }); // When the carousel leaves the page: RingCarousel.destroy(element); // gives back the markup as it was
-
Fit it to your page
- Mark the parts.
data-ring-carouselon the section, then inside it-ringon the box that holds the cards and-cardon each card, a child of the ring. Three cards at least. Optional:-stageon the box the hand grabs (without it, the parent of the ring),-captionon the caption inside each card,-displayon the line that shows the caption of the card in front,-currentand-totalon the two numbers of the counter,-prevand-nexton two buttons. A ring with no card marked takes its own children as the cards. - The width of a card is the measure of the whole ring. The radius is the card width and the
gap, times the number of cards, laid round a circle; the camera standsdepthcard widths away. Give the card a fluid width and the ring follows it: nothing else is sized. - The caption of a card is the content of its
-captionelement, or thealtof its picture when there is none. With the script it is hidden in the card and shown in the-displayline. - One setting for every ring of the page, without opening the file:
window.RingCarouselSettings = { tilt: 14 }in a script placed beforering-carousel.js. Three levels, least specific first:DEFAULTS, then that object, then adata-ring-carousel-*attribute on the section. - Inputs. Mouse and finger through Pointer Events: the stage has
touch-action: pan-y, so a vertical swipe scrolls the page and a sideways one turns the ring. A press that travels less than 6 pixels is a click: the card in front keeps it, so it can be a link, and a card that is not in front comes to the front instead. A trackpad swiped sideways, or a wheel with shift, pushes the ring; a plain vertical wheel is left to the page whenever the page has somewhere to scroll, and turns the ring on a page that has not. The arrow keys step one card, Home and End go to the first and the last: they are heard when the focus is in the section, and also when nothing on the page has the focus and the ring is on screen. - States, written by the engine on the section:
data-ring-carousel-stateisready, orstillunder reduced motion, anddata-ring-carousel-draggingis there while a drag runs. The card in front carriesdata-ring-carousel-active. - Reduced motion: the ring stands as it is, in depth, and nothing glides. A swipe, a wheel notch, a key, a button or a click steps one card at once, the counter and the caption change without rolling, and
autoplayis off. - From JavaScript:
RingCarousel.mount(el, overrides)returns the instance:element,settings,cards,index()(the card in front),rotation()(degrees),speed()(degrees per second),phase(),isRunning(),go(i)(card i to the front on the spring),next(),prev(),select(i)(throws the ring so that its glide docks on card i),refresh()(measures again) anddestroy().destroy()takes off every listener and timer, the veils, the counter and the caption it built, the states, the marks it added and every style it wrote, so the markup is as it was.RingCarousel.destroy(el),RingCarousel.refresh(el),RingCarousel.select(el, i),RingCarousel.auto(root)andRingCarousel.defaultscomplete it. - Events, both bubbling,
detail{ element, index, card, cards }:ringcarousel:changeeach time another card comes in front, once at mount for the first, andringcarousel:settleeach time the ring comes to rest on a card. - The preview ships whole.
snippet.html, the last part ofring-carousel.css, after the linelook, left out of the Webflow Embed, and the pictures ofimages/are the preview, in Geist and Geist Mono, loaded by the font tag of the Code tab. Thering-carousel-*classes are never read by the engine: on cards of your own, keep your classes, add the attributes, and drop that last part of the file and the font tag. - On Webflow, add the fonts first. Add Geist and Geist Mono to your site's fonts (Site settings, Fonts, Google Fonts) before pasting: the paste names them, and the Designer drops a font the site does not have.
- Mark the parts.
-
Avoid the pitfalls
- Nothing between the stage and the cards may flatten the ring.
overflowother than visible,opacityunder 1, afilter, aclip-pathor amaskon the ring box turns the ring into a flat picture, and the cards then overlap in markup order. Clip on the section instead, which is what the preview does. - A section that clips cuts the side cards at its edges. That is the wanted behaviour on a phone, where the ring may be wider than the screen. A narrow column that clips cuts the ring too: give the ring the full width of the page, or a smaller card.
- The ring rises above its box. The far cards stand higher than the card in front, by about the
tilt. Leave air above the stage: the preview gives it more padding at the top than at the foot. - The cards need a width. A card with no width of its own takes the width of its content, and the ring is built from it.
- **Settings handed to
mount()after the page loaded are dropped.**auto()mounts on DOM ready, andmount()on a section already mounted returns the running instance. A page-wide block goes inwindow.RingCarouselSettings, whichauto()reads itself. - Two rings on one screen both hear the arrow keys when nothing has the focus. Focus one of them, by a click or by Tab, and only that one turns.
- A card seen from behind is turned round. Past the side of the ring a card would show its back, its picture mirrored: the engine turns it by half a turn at the moment it is seen edge on, so its face always reads the right way. Do not rely on
backface-visibilityon the cards.
- Nothing between the stage and the cards may flatten the ring.
One click on Copy the AI prompt copies everything an assistant needs to build this effect: the complete code, the markup, the dependency, every setting and every pitfall.
What it does
Ring carousel is a scroll animation for Webflow and vanilla JavaScript, with no library. Also called 3D ring carousel, 3D cylinder carousel or 3D circular carousel.
A carousel in the round. The cards stand on a ring seen from a little above, the one in front at full ink and the others veiled according to their depth. The hand grabs the ring, with the mouse or a finger, and throws it: the ring runs on with the speed it was given, loses it to friction with no notch as the cards go by, and docks on a card, a soft spring taking it for the last degrees. Under the ring, the number of the card in front rolls on a reel geared to the ring, and its caption comes out of a mask as the card comes in. A wheel pushes the ring, the arrow keys and two buttons step it card by card, and a click on a card that is not in front brings it there. The radius is worked out from the width of a card and their number, so the ring follows the page at every size. Under reduced motion the ring stands still and every input steps one card at once. No library: Pointer Events and one transform per frame, on a clock that sleeps at rest. Pattern type 3D carousel, cylinder carousel, rotating gallery.
Updated
Questions
Mark three levels with custom attributes: data-ring-carousel on the section, data-ring-carousel-ring on the box that holds the cards, and data-ring-carousel-card on each card, three at least. Settings go on the section, for instance data-ring-carousel-tilt="12".
No, it loads no library. The drag is read through Pointer Events and the ring is moved by one transform per frame, on a clock that sleeps at rest. The momentum after a throw and the spring that docks a card are computed by the engine.
There is no radius setting: the radius is worked out from the width of a card, the number of cards and data-ring-carousel-gap, the space between two cards as a share of the card width, 0.8 by default. At 0 the cards touch edge to edge, and a higher value gives a wider ring that takes more of the page. Give the card a fluid width and the ring follows it at every size.
Something on the ring box is flattening it: an overflow other than visible, an opacity under 1, a filter, a clip-path or a mask. The ring then becomes a flat picture and the cards overlap in markup order. Clip on the section instead.
No. The stage has touch-action: pan-y, so a vertical swipe scrolls the page and a sideways one turns the ring. A plain vertical mouse wheel is also left to the page whenever the page has somewhere to scroll, and the arrow keys step the ring one card at a time.