A zero-JS carousel component using :has() + radio inputs. Slides are controlled by CSS state machine — no JavaScript required. Supports up to 6 slides.
Flex row holding all slides — animated via translateX on :has() state
vel-carousel-slide
Individual slide — min-width 100%, flex-shrink 0 to fill container
vel-carousel-dots
Dot navigation bar — flex row centered below the track
vel-carousel-dot
Label element targeting a radio input — acts as clickable dot indicator
vel-carousel-arrows
Absolute overlay row for prev/next arrow labels — pointer-events managed
vel-carousel-prev
Previous arrow label — circular button, backdrop blur, left side
vel-carousel-next
Next arrow label — circular button, backdrop blur, right side
vel-carousel-img
Modifier on vel-carousel — sets slides to relative for caption overlay
vel-carousel-caption
Absolute overlay at slide bottom — gradient fade from black
vel-carousel-caption-title
Bold heading text inside vel-carousel-caption
vel-carousel-caption-sub
Muted subtitle text inside vel-carousel-caption
Examples
Card content carousel
Slide 1 — Color Genetics
One hue drives every color on the page via oklch(). Change --vel-dna-hue and 50+ colors update instantly.
Slide 2 — CSS State Machine
Zero-JS tabs, toggles, and carousels using :has() + radio inputs. No JavaScript required for state management.
:has()Zero JSradio inputs
Slide 3 — Fluid Scale
All spacing and typography tokens use clamp() — smooth scaling between breakpoints with zero media queries.
example.html
<divstyle="padding:24px;background:#060b17;border-radius:12px;font-family:system-ui,sans-serif;"><divclass="vel-carousel"><inputtype="radio"name="demo-carousel"id="vel-slide-1"checked><inputtype="radio"name="demo-carousel"id="vel-slide-2"><inputtype="radio"name="demo-carousel"id="vel-slide-3"><divclass="vel-carousel-track"><divclass="vel-carousel-slide"><divclass="vel-cardvel-p-6"style="border-radius:0;border:none;min-height:180px"><divclass="vel-text-primaryvel-font-boldvel-mb-2">Slide 1 — Color Genetics</div><divclass="vel-text-mutedvel-text-sm">One hue drives every color on the page via oklch(). Change --vel-dna-hue and 50+ colors update instantly.</div><divclass="vel-flexvel-gap-2vel-mt-4"><divstyle="width:32px;height:32px;border-radius:50%;background:var(--vel-color-primary)"></div><divstyle="width:32px;height:32px;border-radius:50%;background:var(--vel-color-success)"></div><divstyle="width:32px;height:32px;border-radius:50%;background:var(--vel-color-danger)"></div></div></div></div><divclass="vel-carousel-slide"><divclass="vel-cardvel-p-6"style="border-radius:0;border:none;min-height:180px"><divclass="vel-text-primaryvel-font-boldvel-mb-2">Slide 2 — CSS State Machine</div><divclass="vel-text-mutedvel-text-sm">Zero-JS tabs, toggles, and carousels using :has() + radio inputs. No JavaScript required for state management.</div><divclass="vel-flexvel-gap-2vel-mt-4"><spanclass="vel-badgevel-badge-primary">:has()</span><spanclass="vel-badgevel-badge-success">Zero JS</span><spanclass="vel-badgevel-badge-warning">radio inputs</span></div></div></div><divclass="vel-carousel-slide"><divclass="vel-cardvel-p-6"style="border-radius:0;border:none;min-height:180px"><divclass="vel-text-primaryvel-font-boldvel-mb-2">Slide 3 — Fluid Scale</div><divclass="vel-text-mutedvel-text-sm">All spacing and typography tokens use clamp() — smooth scaling between breakpoints with zero media queries.</div><divclass="vel-flexvel-gap-2vel-mt-4"><buttonclass="vel-btnvel-btn-primaryvel-btn-sm">Primary</button><buttonclass="vel-btnvel-btn-secondaryvel-btn-sm">Secondary</button></div></div></div></div><divclass="vel-carousel-dots"><labelclass="vel-carousel-dot"for="vel-slide-1">1</label><labelclass="vel-carousel-dot"for="vel-slide-2">2</label><labelclass="vel-carousel-dot"for="vel-slide-3">3</label></div></div></div>