What's included
A carousel of images set on the rim of a giant wheel, in two files: a stylesheet and a script. No dependencies, no build step, no external requests — fonts and images live in the component's folder.
The card at the top grows, lifts and speaks up; the others tilt, shrink and fade as they travel down the arc. All of it derives from a single number,
--wcar-index, which is the selected card. Because that number is declared with @property, animating it from one card to the next turns the whole wheel, resizes it, reorders the layers and moves the text — in a single movement, since everything descends from it.
Features
-
Works without JavaScript — each card is the
<label>of a radio button. Clicking a card selects it, the arrow keys move through the group, and the wheel turns: it's the CSS that reads the selection with:has(). - Full keyboard support — the radio group brings its native traversal; the focus ring is painted on the card, not on the control. Focus follows the navigation arrows when the caption changes.
- Grab, spin, release — the wheel can be grabbed anywhere: on the background, on a card, on the text. Beyond five pixels the press becomes a drag; below that, it stays a click that selects. The wheel then settles on the nearest card with a spring curve, sampled from the original.
- The count is read from the markup — two cards or twelve, the arc spreads itself out. Nothing to declare, nothing to recalculate.
-
One variable for the scale — radius, cards, text, dots and lift all derive from
--wcar-font-size; the arc itself carries on following the width of its container. - Breakpoints on the container — the arc opens less and the cards take up more room in a narrow column, even if the window itself is wide.
Dropping it into a page
Two files to load, then one card per image:
<link rel="stylesheet" href="arc-carousel.css">
<script src="arc-carousel.js" defer></script>
<section class="arc-carousel" data-arc-carousel aria-label="Moods">
<div class="arc-carousel__stage">
<div class="arc-carousel__halo"></div>
<div class="arc-carousel__track" role="radiogroup"
aria-label="Choose an image">
<div class="arc-carousel__item">
<label class="arc-carousel__card">
<input class="arc-carousel__radio" type="radio"
name="roue" checked>
<img class="arc-carousel__image" src="medias/salon.webp" alt="">
<span class="arc-carousel__veil"></span>
<span class="arc-carousel__name">Soft minimalism</span>
</label>
<div class="arc-carousel__caption">
<div class="arc-carousel__panel">
<div class="arc-carousel__headline">
<button class="arc-carousel__arrow" type="button"
data-wcar-prev aria-label="Previous">…</button>
<p class="arc-carousel__title">Soft minimalism</p>
<button class="arc-carousel__arrow" type="button"
data-wcar-next aria-label="Next image">…</button>
</div>
<p class="arc-carousel__text">Small, carefully chosen spaces.</p>
<a class="arc-carousel__cta" href="#">Explore</a>
</div>
</div>
</div>
<!-- … the other cards … -->
</div>
<div class="arc-carousel__dots" aria-hidden="true">
<span class="arc-carousel__dot"></span>
</div>
<span class="arc-carousel__probe" aria-hidden="true"></span>
<p class="arc-carousel__status" role="status" aria-live="polite"></p>
</div>
</section>
A single checked in the group, a different name
per carousel on the page, and that's all: the component counts the cards itself.
What you should know
-
The wheel takes the height you give it.
By default
100svh; the position of the text block is worked out from that height, so changing it moves the text, never the arc. That's--wcar-height. -
Half the wheel is outside the frame. That's the design: the cards at either end leave through the sides. The frame is set to
overflow: clip— so it can't scroll, and focus landing on an off-frame card doesn't shift the layout. - Without the script, the two arrows disappear. They would ask for “the next card”, which a stylesheet has no way of expressing. Everything else — selecting by click, keyboard traversal, rotation, captions — carries on working.
-
No inline styles, no inline scripts. The component honours the site's strict security policy, and never uses
innerHTML. The script writes its position throughstyle.setProperty(), which the policy allows. - The design comes from elsewhere. The proportions follow those of a published Framer component, measured in the browser on the live site at five widths. The code itself is original: the original is a React carousel that recomputes ten positions every frame, this one is a stylesheet that derives everything from one number.
Take it, open it, break it.
This component is free. The full library — the templates and the other components — comes with the subscription, alongside the course.
Immediate access · No commitment · Cancel in one click