UI Components Medium
Carousel
Content carousel with auto-play, dot indicators, previous/next arrows, touch swipe support, and a multi-card visible variant.
Open in Lab
MCP
css vanilla-js
Targets: JS HTML React Native
Expo Snack
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--bg: #050910;
--card: #0d1117;
--border: rgba(255, 255, 255, 0.08);
--text: #f2f6ff;
--muted: #475569;
--accent: #38bdf8;
--radius: 14px;
--transition: 0.42s cubic-bezier(0.4, 0, 0.2, 1);
}
body {
background: var(--bg);
color: var(--text);
font-family: system-ui, -apple-system, sans-serif;
min-height: 100vh;
padding: 3rem 1.5rem;
}
.page {
max-width: 900px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 3.5rem;
}
.demo-section {
display: flex;
flex-direction: column;
gap: 1rem;
}
.demo-label {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--muted);
}
/* โโ Carousel shell โโ */
.carousel {
position: relative;
outline: none;
}
.carousel-viewport {
overflow: hidden;
border-radius: var(--radius);
}
.carousel-track {
display: flex;
transition: transform var(--transition);
will-change: transform;
}
/* โโ Single-item slides โโ */
.carousel-slide {
min-width: 100%;
height: 340px;
display: flex;
align-items: flex-end;
padding: 2rem;
border-radius: var(--radius);
position: relative;
overflow: hidden;
}
.slide--1 {
background: linear-gradient(135deg, #0f2044 0%, #0a3a5c 100%);
}
.slide--2 {
background: linear-gradient(135deg, #1a0a3c 0%, #3b0f6e 100%);
}
.slide--3 {
background: linear-gradient(135deg, #072a1e 0%, #0d4a32 100%);
}
.slide--4 {
background: linear-gradient(135deg, #2a0f0a 0%, #5c1f10 100%);
}
.slide--5 {
background: linear-gradient(135deg, #1a1a0a 0%, #3d3800 100%);
}
.slide-content {
position: relative;
z-index: 1;
}
.slide-badge {
display: inline-block;
font-size: 0.7rem;
font-weight: 700;
color: var(--accent);
border: 1px solid rgba(56, 189, 248, 0.3);
border-radius: 4px;
padding: 0.2em 0.5em;
margin-bottom: 0.75rem;
letter-spacing: 0.1em;
}
.slide-content h3 {
font-size: 1.6rem;
font-weight: 700;
margin-bottom: 0.5rem;
letter-spacing: -0.02em;
}
.slide-content p {
color: rgba(242, 246, 255, 0.65);
font-size: 0.9rem;
max-width: 360px;
line-height: 1.6;
}
/* โโ Arrows โโ */
.carousel-arrow {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 40px;
height: 40px;
border-radius: 50%;
background: rgba(13, 17, 23, 0.8);
border: 1px solid var(--border);
color: var(--text);
font-size: 1.4rem;
cursor: pointer;
display: grid;
place-items: center;
transition: border-color 0.2s, background 0.2s, opacity 0.2s;
z-index: 10;
backdrop-filter: blur(6px);
line-height: 1;
}
.carousel-arrow--prev {
left: 0.75rem;
}
.carousel-arrow--next {
right: 0.75rem;
}
.carousel-arrow:hover {
border-color: var(--accent);
background: rgba(56, 189, 248, 0.12);
}
.carousel-arrow:disabled {
opacity: 0.3;
cursor: not-allowed;
}
/* โโ Dots โโ */
.carousel-dots {
display: flex;
justify-content: center;
gap: 0.5rem;
margin-top: 1rem;
}
.carousel-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
border: none;
cursor: pointer;
padding: 0;
transition: background 0.25s, transform 0.25s, width 0.25s;
}
.carousel-dot.active {
background: var(--accent);
width: 18px;
border-radius: 3px;
}
/* โโ Multi-card โโ */
.carousel-viewport--multi {
border-radius: 0;
}
.carousel--multi .carousel-track {
gap: 1rem;
}
.mc-card {
min-width: calc((100% - 2rem) / 3);
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.5rem;
flex-shrink: 0;
}
.mc-card__icon {
font-size: 1.5rem;
margin-bottom: 0.75rem;
}
.mc-card h4 {
font-size: 1rem;
font-weight: 600;
margin-bottom: 0.4rem;
}
.mc-card p {
font-size: 0.825rem;
color: var(--muted);
line-height: 1.55;
}
@media (max-width: 600px) {
.mc-card {
min-width: 80%;
}
.carousel-slide {
height: 240px;
}
}(function () {
"use strict";
// โโ Single-item carousel โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
initCarousel({
id: "single",
mode: "single",
autoPlay: true,
autoPlayInterval: 3500,
});
// โโ Multi-card carousel โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
initCarousel({
id: "multi",
mode: "multi",
visibleCount: 3,
autoPlay: false,
});
// โโ Factory โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function initCarousel(opts) {
const root = document.getElementById("carousel-" + opts.id);
const track = document.getElementById("track-" + opts.id);
const dotsEl = document.getElementById("dots-" + opts.id);
const prevBtn = document.getElementById("prev-" + opts.id);
const nextBtn = document.getElementById("next-" + opts.id);
if (!root || !track) return;
const slides = Array.from(track.children);
const total = slides.length;
const visible = opts.visibleCount || 1;
const maxIndex = Math.max(0, total - visible);
let current = 0;
let autoTimer = null;
// Build dots
const dots = [];
for (let i = 0; i <= maxIndex; i++) {
const btn = document.createElement("button");
btn.className = "carousel-dot";
btn.setAttribute("role", "tab");
btn.setAttribute("aria-label", "Go to slide " + (i + 1));
btn.addEventListener("click", () => goTo(i));
dotsEl.appendChild(btn);
dots.push(btn);
}
function goTo(index) {
current = Math.max(0, Math.min(index, maxIndex));
updateTrack();
updateDots();
updateArrows();
}
function updateTrack() {
if (opts.mode === "multi") {
// Calculate offset: each card width + gap
const cardW = track.firstElementChild.offsetWidth;
const gap = 16; // 1rem
track.style.transform = "translateX(-" + current * (cardW + gap) + "px)";
} else {
track.style.transform = "translateX(-" + current * 100 + "%)";
}
}
function updateDots() {
dots.forEach((d, i) => {
d.classList.toggle("active", i === current);
d.setAttribute("aria-selected", i === current ? "true" : "false");
});
}
function updateArrows() {
if (prevBtn) prevBtn.disabled = current === 0;
if (nextBtn) nextBtn.disabled = current === maxIndex;
}
function prev() {
goTo(current - 1);
}
function next() {
goTo(current + 1);
}
if (prevBtn) prevBtn.addEventListener("click", prev);
if (nextBtn) nextBtn.addEventListener("click", next);
// Keyboard
root.addEventListener("keydown", function (e) {
if (e.key === "ArrowLeft") {
e.preventDefault();
prev();
}
if (e.key === "ArrowRight") {
e.preventDefault();
next();
}
});
// Touch swipe
let touchStartX = 0;
root.addEventListener(
"touchstart",
function (e) {
touchStartX = e.touches[0].clientX;
},
{ passive: true }
);
root.addEventListener(
"touchend",
function (e) {
const dx = e.changedTouches[0].clientX - touchStartX;
if (Math.abs(dx) > 50) dx < 0 ? next() : prev();
},
{ passive: true }
);
// Auto-play
if (opts.autoPlay) {
function startAuto() {
autoTimer = setInterval(function () {
goTo(current < maxIndex ? current + 1 : 0);
}, opts.autoPlayInterval || 4000);
}
function stopAuto() {
clearInterval(autoTimer);
}
root.addEventListener("mouseenter", stopAuto);
root.addEventListener("mouseleave", startAuto);
root.addEventListener("focusin", stopAuto);
root.addEventListener("focusout", startAuto);
startAuto();
}
// Init
goTo(0);
}
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Carousel</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<!-- โโ Demo 1: Single-item carousel โโ -->
<section class="demo-section">
<h2 class="demo-label">Single-item carousel</h2>
<div class="carousel" id="carousel-single" data-mode="single" tabindex="0" aria-label="Image carousel">
<div class="carousel-viewport">
<div class="carousel-track" id="track-single">
<div class="carousel-slide slide--1">
<div class="slide-content">
<span class="slide-badge">01</span>
<h3>Design Systems</h3>
<p>Build scalable, consistent UI with reusable components and tokens.</p>
</div>
</div>
<div class="carousel-slide slide--2">
<div class="slide-content">
<span class="slide-badge">02</span>
<h3>Motion Design</h3>
<p>Bring interfaces to life with purposeful, physics-based animations.</p>
</div>
</div>
<div class="carousel-slide slide--3">
<div class="slide-content">
<span class="slide-badge">03</span>
<h3>Web Performance</h3>
<p>Ship faster experiences with smart loading and critical path optimisation.</p>
</div>
</div>
<div class="carousel-slide slide--4">
<div class="slide-content">
<span class="slide-badge">04</span>
<h3>Accessibility</h3>
<p>Design and code for every user โ keyboard, screen reader, and beyond.</p>
</div>
</div>
<div class="carousel-slide slide--5">
<div class="slide-content">
<span class="slide-badge">05</span>
<h3>Developer UX</h3>
<p>Great APIs, great docs, great DX โ the invisible layer that wins hearts.</p>
</div>
</div>
</div>
</div>
<!-- Arrows -->
<button class="carousel-arrow carousel-arrow--prev" id="prev-single" aria-label="Previous slide">โน</button>
<button class="carousel-arrow carousel-arrow--next" id="next-single" aria-label="Next slide">โบ</button>
<!-- Dots -->
<div class="carousel-dots" id="dots-single" role="tablist" aria-label="Slide indicators"></div>
</div>
</section>
<!-- โโ Demo 2: Multi-card carousel โโ -->
<section class="demo-section">
<h2 class="demo-label">Multi-card carousel (3 visible)</h2>
<div class="carousel carousel--multi" id="carousel-multi" data-mode="multi" tabindex="0" aria-label="Cards carousel">
<div class="carousel-viewport carousel-viewport--multi">
<div class="carousel-track" id="track-multi">
<div class="mc-card">
<div class="mc-card__icon">โก</div>
<h4>Fast</h4>
<p>Zero dependencies, tiny footprint, instant paint.</p>
</div>
<div class="mc-card">
<div class="mc-card__icon">๐จ</div>
<h4>Beautiful</h4>
<p>Dark-first aesthetics with a coherent design language.</p>
</div>
<div class="mc-card">
<div class="mc-card__icon">โฟ</div>
<h4>Accessible</h4>
<p>ARIA roles, keyboard navigation, focus management.</p>
</div>
<div class="mc-card">
<div class="mc-card__icon">๐ฑ</div>
<h4>Responsive</h4>
<p>Adapts from mobile screens to wide desktop layouts.</p>
</div>
<div class="mc-card">
<div class="mc-card__icon">๐</div>
<h4>Secure</h4>
<p>No eval, no remote scripts, runs fully client-side.</p>
</div>
<div class="mc-card">
<div class="mc-card__icon">๐</div>
<h4>Global</h4>
<p>i18n-ready with RTL layout support out of the box.</p>
</div>
<div class="mc-card">
<div class="mc-card__icon">๐ ๏ธ</div>
<h4>Customisable</h4>
<p>CSS custom properties for every visual detail.</p>
</div>
</div>
</div>
<button class="carousel-arrow carousel-arrow--prev" id="prev-multi" aria-label="Previous">โน</button>
<button class="carousel-arrow carousel-arrow--next" id="next-multi" aria-label="Next">โบ</button>
<div class="carousel-dots" id="dots-multi" role="tablist"></div>
</div>
</section>
</div>
<script src="script.js"></script>
</body>
</html>import React, { useCallback, useEffect, useRef, useState } from "react";
import {
Dimensions,
Image,
NativeScrollEvent,
NativeSyntheticEvent,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from "react-native";
const { width: SCREEN_WIDTH } = Dimensions.get("window");
const SLIDE_WIDTH = SCREEN_WIDTH - 40;
const SLIDE_MARGIN = 20;
interface CarouselProps<T> {
data: T[];
renderItem: (item: T, index: number) => React.ReactNode;
autoPlay?: boolean;
interval?: number;
}
function Carousel<T>({ data, renderItem, autoPlay = false, interval = 3000 }: CarouselProps<T>) {
const [activeIndex, setActiveIndex] = useState(0);
const scrollRef = useRef<ScrollView>(null);
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const scrollToIndex = useCallback((index: number) => {
scrollRef.current?.scrollTo({
x: index * SCREEN_WIDTH,
animated: true,
});
}, []);
useEffect(() => {
if (!autoPlay) return;
timerRef.current = setInterval(() => {
setActiveIndex((prev) => {
const next = (prev + 1) % data.length;
scrollToIndex(next);
return next;
});
}, interval);
return () => {
if (timerRef.current) clearInterval(timerRef.current);
};
}, [autoPlay, interval, data.length, scrollToIndex]);
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
const index = Math.round(e.nativeEvent.contentOffset.x / SCREEN_WIDTH);
if (index !== activeIndex && index >= 0 && index < data.length) {
setActiveIndex(index);
}
};
return (
<View>
<ScrollView
ref={scrollRef}
horizontal
pagingEnabled
showsHorizontalScrollIndicator={false}
onScroll={onScroll}
scrollEventThrottle={16}
>
{data.map((item, index) => (
<View key={index} style={styles.slide}>
{renderItem(item, index)}
</View>
))}
</ScrollView>
{/* Dot indicators */}
<View style={styles.dots}>
{data.map((_, index) => (
<TouchableOpacity
key={index}
onPress={() => {
setActiveIndex(index);
scrollToIndex(index);
}}
>
<View style={[styles.dot, index === activeIndex && styles.dotActive]} />
</TouchableOpacity>
))}
</View>
</View>
);
}
interface SlideData {
id: number;
imageUri: string;
title: string;
}
const SLIDES: SlideData[] = [
{ id: 1, imageUri: "https://picsum.photos/seed/slide1/600/400", title: "Mountain Vista" },
{ id: 2, imageUri: "https://picsum.photos/seed/slide2/600/400", title: "Ocean Breeze" },
{ id: 3, imageUri: "https://picsum.photos/seed/slide3/600/400", title: "Forest Trail" },
{ id: 4, imageUri: "https://picsum.photos/seed/slide4/600/400", title: "Desert Dunes" },
{ id: 5, imageUri: "https://picsum.photos/seed/slide5/600/400", title: "City Lights" },
];
export default function App() {
return (
<View style={styles.container}>
<Text style={styles.header}>Discover</Text>
<Text style={styles.subheader}>Swipe through featured destinations</Text>
<Carousel
data={SLIDES}
autoPlay
interval={4000}
renderItem={(item) => (
<View style={styles.card}>
<Image source={{ uri: item.imageUri }} style={styles.image} />
<View style={styles.cardOverlay}>
<Text style={styles.cardTitle}>{item.title}</Text>
</View>
</View>
)}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#0f172a",
paddingTop: 60,
},
header: {
color: "#f8fafc",
fontSize: 28,
fontWeight: "700",
paddingHorizontal: 20,
},
subheader: {
color: "#64748b",
fontSize: 14,
paddingHorizontal: 20,
marginBottom: 24,
marginTop: 4,
},
slide: {
width: SCREEN_WIDTH,
paddingHorizontal: SLIDE_MARGIN,
},
card: {
borderRadius: 16,
overflow: "hidden",
backgroundColor: "#1e293b",
},
image: {
width: SLIDE_WIDTH,
height: 240,
},
cardOverlay: {
padding: 16,
},
cardTitle: {
color: "#f8fafc",
fontSize: 18,
fontWeight: "600",
},
dots: {
flexDirection: "row",
justifyContent: "center",
marginTop: 16,
gap: 8,
},
dot: {
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: "#334155",
},
dotActive: {
backgroundColor: "#818cf8",
width: 24,
},
});Carousel
A fully-featured content carousel with two layout modes. Built with CSS transform: translateX and vanilla JS โ no external libraries.
Variants
- Single-item โ one slide fills the viewport, with prev/next arrows, dot indicators, and auto-play
- Multi-card โ three cards visible at once, scrolling one at a time
How it works
.carousel-trackis a flex row; the track translates on the X axis to reveal slidesgoTo(index)calculates the correcttranslateXoffset and applies a CSS transition- Auto-play uses
setInterval; hovering the carousel pauses it and restores onmouseleave - Touch swipe is detected via
touchstart/touchenddelta โ a swipe of >50px triggers prev/next - Dots sync with the current index; clicking a dot jumps directly to that slide
Keyboard
- Left/Right arrow keys navigate slides when the carousel has focus
- Auto-play pauses on focus-within for accessibility