Skip to content

Instantly share code, notes, and snippets.

@hackur
Created April 29, 2025 23:45
Show Gist options
  • Save hackur/91a9336a0f03b97d52c14e477bf7175c to your computer and use it in GitHub Desktop.
Save hackur/91a9336a0f03b97d52c14e477bf7175c to your computer and use it in GitHub Desktop.
CodePen Challenge: Card Carousel
<h1 class="title">Travel Destinations 2025</h1>
<p class="subtitle">Explore our hand-picked destinations for your next adventure</p>
<div class="carousel-container">
<button class="nav-button prev-btn" aria-label="Previous destination"></button>
<div class="carousel" id="carousel">
<!-- Card 1 -->
<div class="card kyoto">
<img class="card-image" src="https://assets.codepen.io/406785/kyoto.jpg" alt="" />
<div class="card-badge">Popular</div>
<div class="card-header">
<h2 class="card-title">Kyoto, Japan</h2>
<p class="card-subtitle">Traditional Japanese Experience</p>
</div>
<div class="card-content">
<p>Discover ancient temples, stunning gardens, and traditional tea houses. Experience the beauty of cherry blossoms in spring or vibrant autumn colours.</p>
</div>
<div class="card-meta">
<div class="card-rating">
<span class="stars">★★★★★</span> 4.8
</div>
<span>7-12 days</span>
</div>
<div class="card-footer">
<div class="price">
<span class="original-price">$2,400</span>
<span>$1,950</span>
<span class="discount">(-19%)</span>
</div>
<button class="card-button">Explore Package</button>
</div>
</div>
<!-- Card 2 -->
<div class="card santorini">
<img class="card-image" src="https://assets.codepen.io/406785/santorini.jpg" alt="" />
<div class="card-badge">Best Value</div>
<div class="card-header">
<h2 class="card-title">Santorini, Greece</h2>
<p class="card-subtitle">Mediterranean Paradise</p>
</div>
<div class="card-content">
<p>White-washed buildings, blue domes, and breathtaking sunsets over the Aegean Sea. Enjoy local cuisine and explore charming villages.</p>
</div>
<div class="card-meta">
<div class="card-rating">
<span class="stars">★★★★★</span> 4.9
</div>
<span>5-10 days</span>
</div>
<div class="card-footer">
<div class="price">
<span class="original-price">$2,200</span>
<span>$1,850</span>
<span class="discount">(-16%)</span>
</div>
<button class="card-button">Explore Package</button>
</div>
</div>
<!-- Card 3 -->
<div class="card banff">
<img class="card-image" src="https://assets.codepen.io/406785/banff.webp" alt="" />
<div class="card-badge">New</div>
<div class="card-header">
<h2 class="card-title">Banff, Canada</h2>
<p class="card-subtitle">Mountain Adventure</p>
</div>
<div class="card-content">
<p>Turquoise lakes, snow-capped mountains and abundant wildlife. Perfect for hiking, skiing, and photography in this pristine wilderness.</p>
</div>
<div class="card-meta">
<div class="card-rating">
<span class="stars">★★★★☆</span> 4.7
</div>
<span>6-14 days</span>
</div>
<div class="card-footer">
<div class="price">
<span class="original-price">$2,150</span>
<span>$1,780</span>
<span class="discount">(-17%)</span>
</div>
<button class="card-button">Explore Package</button>
</div>
</div>
<!-- Card 4 -->
<div class="card bali">
<img class="card-image" src="https://assets.codepen.io/406785/bali.webp" alt="" />
<div class="card-badge">Trending</div>
<div class="card-header">
<h2 class="card-title">Bali, Indonesia</h2>
<p class="card-subtitle">Tropical Paradise</p>
</div>
<div class="card-content">
<p>Lush rice terraces, sacred temples, and pristine beaches. Immerse yourself in Balinese culture and enjoy world-class surfing spots.</p>
</div>
<div class="card-meta">
<div class="card-rating">
<span class="stars">★★★★☆</span> 4.5
</div>
<span>7-14 days</span>
</div>
<div class="card-footer">
<div class="price">
<span class="original-price">$1,950</span>
<span>$1,550</span>
<span class="discount">(-21%)</span>
</div>
<button class="card-button">Explore Package</button>
</div>
</div>
<!-- Card 5 -->
<div class="card marrakech">
<img class="card-image" src="https://assets.codepen.io/406785/marrakech.webp" alt="" />
<div class="card-badge">Cultural</div>
<div class="card-header">
<h2 class="card-title">Marrakech, Morocco</h2>
<p class="card-subtitle">Desert Exploration</p>
</div>
<div class="card-content">
<p>Vibrant markets, historic medinas, and spectacular desert landscapes. Experience luxury riads and traditional Moroccan hospitality.</p>
</div>
<div class="card-meta">
<div class="card-rating">
<span class="stars">★★★★☆</span> 4.6
</div>
<span>5-10 days</span>
</div>
<div class="card-footer">
<div class="price">
<span class="original-price">$1,800</span>
<span>$1,490</span>
<span class="discount">(-17%)</span>
</div>
<button class="card-button">Explore Package</button>
</div>
</div>
<!-- Card 6 -->
<div class="card machupicchu">
<img class="card-image" src="https://assets.codepen.io/406785/machupicchu.jpg" alt="" />
<div class="card-badge">Adventure</div>
<div class="card-header">
<h2 class="card-title">Machu Picchu, Peru</h2>
<p class="card-subtitle">Ancient Wonder</p>
</div>
<div class="card-content">
<p>Hike the legendary Inca Trail to discover this archaeological marvel. Explore the Sacred Valley and experience Peruvian cuisine.</p>
</div>
<div class="card-meta">
<div class="card-rating">
<span class="stars">★★★★★</span> 4.9
</div>
<span>9-15 days</span>
</div>
<div class="card-footer">
<div class="price">
<span class="original-price">$2,600</span>
<span>$2,180</span>
<span class="discount">(-16%)</span>
</div>
<button class="card-button">Explore Package</button>
</div>
</div>
</div>
<button class="nav-button next-btn" aria-label="Next destination"></button>
</div>
document.addEventListener('DOMContentLoaded', function() {
const carousel = document.getElementById('carousel');
const cards = Array.from(carousel.querySelectorAll('.card'));
const prevBtn = document.querySelector('.prev-btn');
const nextBtn = document.querySelector('.next-btn');
let currentIndex = 0;
let isScrolling = false;
function scrollToCard(index) {
if (index < 0) index = 0;
if (index >= cards.length) index = cards.length - 1;
if (isScrolling) return;
currentIndex = index;
isScrolling = true;
const card = cards[index];
const cardWidth = card.offsetWidth;
const cardLeft = card.offsetLeft;
const carouselWidth = carousel.offsetWidth;
// Center the card in the viewport
const scrollPosition = cardLeft - (carouselWidth / 2) + (cardWidth / 2);
// Use smoother animation with more steps
const startPosition = carousel.scrollLeft;
const distance = scrollPosition - startPosition;
const duration = 500; // ms
const steps = 30;
const delay = duration / steps;
let currentStep = 0;
const smoothScroll = setInterval(() => {
currentStep++;
if (currentStep > steps) {
clearInterval(smoothScroll);
isScrolling = false;
return;
}
// Use easeInOutCubic easing function for smoother animation
let progress = currentStep / steps;
progress = progress < 0.5
? 4 * progress * progress * progress
: 1 - Math.pow(-2 * progress + 2, 3) / 2;
const currentPosition = startPosition + distance * progress;
carousel.scrollLeft = currentPosition;
}, delay);
}
// Find which card is most visible after scrolling
function updateCurrentIndex() {
if (isScrolling) return;
const scrollPosition = carousel.scrollLeft;
const viewportMiddle = scrollPosition + (carousel.offsetWidth / 2);
let closestIndex = 0;
let closestDistance = Math.abs(cards[0].offsetLeft + (cards[0].offsetWidth / 2) - viewportMiddle);
cards.forEach((card, index) => {
const cardMiddle = card.offsetLeft + (card.offsetWidth / 2);
const distance = Math.abs(cardMiddle - viewportMiddle);
if (distance < closestDistance) {
closestDistance = distance;
closestIndex = index;
}
});
if (closestIndex !== currentIndex) {
currentIndex = closestIndex;
}
}
// Scroll to initial card (first card)
setTimeout(() => {
scrollToCard(0);
}, 100);
// Previous button
prevBtn.addEventListener('click', () => {
scrollToCard(currentIndex - 1);
});
// Next button
nextBtn.addEventListener('click', () => {
scrollToCard(currentIndex + 1);
});
// Update current index on scroll
carousel.addEventListener('scroll', () => {
if (!isScrolling) {
updateCurrentIndex();
}
});
// Debounce function to limit scroll events
function debounce(func, wait) {
let timeout;
return function() {
const context = this;
const args = arguments;
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(context, args);
}, wait);
};
}
// Use debounced version for scroll end detection
const handleScrollEnd = debounce(() => {
isScrolling = false;
updateCurrentIndex();
}, 100);
carousel.addEventListener('scroll', handleScrollEnd);
// Keyboard navigation
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft') {
scrollToCard(currentIndex - 1);
} else if (e.key === 'ArrowRight') {
scrollToCard(currentIndex + 1);
}
});
// Touch swipe support
let touchStartX = 0;
carousel.addEventListener('touchstart', (e) => {
touchStartX = e.changedTouches[0].screenX;
});
carousel.addEventListener('touchend', (e) => {
const touchEndX = e.changedTouches[0].screenX;
const swipeThreshold = 50;
if (touchStartX - touchEndX > swipeThreshold) {
// Swipe left
scrollToCard(currentIndex + 1);
} else if (touchEndX - touchStartX > swipeThreshold) {
// Swipe right
scrollToCard(currentIndex - 1);
}
});
});
:root {
/* Base colors */
--primary-color: #e67e22;
--secondary-color: #d35400;
--card-bg: #ffffff;
--card-text: #333333;
--card-shadow: rgba(0, 0, 0, 0.15);
--active-card-shadow: rgba(230, 126, 34, 0.4);
--carousel-bg: #fffaf5;
/* Destination-specific colors */
--kyoto-color: #c0392b;
--kyoto-color-hover: #a93226;
--santorini-color: #2980b9;
--santorini-color-hover: #2471a3;
--banff-color: #27ae60;
--banff-color-hover: #229954;
--bali-color: #d35400;
--bali-color-hover: #ba4a00;
--marrakech-color: #8e44ad;
--marrakech-color-hover: #7d3c98;
--machupicchu-color: #16a085;
--machupicchu-color-hover: #138d75;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #e67e22 0%, #8e44ad 100%);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 100vh;
color: var(--card-text);
padding: 2rem 1rem;
}
.title {
color: #fff9f0;
text-align: center;
margin-bottom: 2rem;
font-size: 2.2rem;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
font-weight: 800;
}
.subtitle {
color: #fff9f0;
text-align: center;
margin-top: -1rem;
margin-bottom: 2rem;
font-size: 1.1rem;
}
.carousel-container {
position: relative;
width: 100%;
max-width: 1200px;
}
/* Carousel */
.carousel {
display: flex;
gap: 20px;
width: 100%;
max-width: 90vw;
background-color: var(--carousel-bg);
border-radius: 12px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
padding: 20px;
overflow-x: auto;
scroll-snap-type: x mandatory;
scrollbar-width: none; /* Hide scrollbar for Firefox */
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
position: relative;
}
.carousel::-webkit-scrollbar {
display: none;
}
/* Updated navigation buttons */
.nav-button {
position: absolute;
top: 50%;
transform: translateY(-50%);
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 50%;
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
transition: all 0.3s ease;
font-size: 1.5rem;
z-index: 10;
opacity: 1;
}
.prev-btn {
left: -25px; /* Position outside the carousel */
}
.next-btn {
right: -25px; /* Position outside the carousel */
}
.nav-button:hover {
background-color: var(--secondary-color);
transform: translateY(-50%) scale(1.1);
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.4);
}
/* Card styling */
.card {
flex: 0 0 auto;
width: 300px;
min-height: 520px;
display: flex;
flex-direction: column;
background-color: var(--card-bg);
border-radius: 12px;
padding: 0;
box-shadow: 0 4px 12px var(--card-shadow);
transition: all 0.3s ease;
scroll-snap-align: center;
position: relative;
}
.card-image {
width: 100%;
height: 180px;
object-fit: cover;
object-position: center;
border-top-left-radius: 12px;
border-top-right-radius: 12px;
display: block;
background-color: #f0f0f0;
}
.card-badge {
position: absolute;
top: 12px;
right: 12px;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: bold;
z-index: 2;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
color: white;
}
.card-header {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
padding: 1.2rem 1.2rem 0.5rem;
}
.card-title {
font-size: 1.5rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
.card-subtitle {
font-size: 0.9rem;
color: #666;
margin-bottom: 0.5rem;
}
.card-content {
font-size: 0.95rem;
line-height: 1.5;
color: #555;
padding: 0 1.2rem;
text-align: left;
margin-bottom: 15px;
flex-grow: 1;
}
.card-content p {
margin: 0;
padding: 0;
}
.card-info-container {
display: flex;
flex-direction: column;
margin-top: auto;
}
.card-meta {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 0.8rem 1.2rem;
font-size: 0.85rem;
color: #777;
border-top: 1px solid rgba(0, 0, 0, 0.05);
background-color: rgba(0, 0, 0, 0.02);
}
.card-rating {
display: flex;
align-items: center;
}
.stars {
color: gold;
margin-right: 5px;
}
.card-footer {
width: 100%;
padding: 1rem 1.2rem 1.5rem;
background-color: rgba(0, 0, 0, 0.02);
border-top: 1px solid rgba(0, 0, 0, 0.05);
}
.price {
font-size: 1.2rem;
font-weight: bold;
margin-bottom: 0.7rem;
}
.original-price {
text-decoration: line-through;
color: #999;
font-size: 0.9rem;
margin-right: 8px;
}
.discount {
color: #e53935;
font-weight: bold;
}
.card-button {
background-color: #e67e22;
color: white;
border: none;
border-radius: 30px;
padding: 0.75rem 1.5rem;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.3s;
width: 100%;
font-weight: 600;
display: block;
margin-top: 10px;
}
.card-button:hover {
background-color: #d35400;
}
/* Destination-specific styling with variables and classes */
.kyoto .card-badge { background-color: var(--kyoto-color); }
.kyoto .card-title, .kyoto .price { color: var(--kyoto-color); }
.kyoto .card-button { background-color: var(--kyoto-color); }
.kyoto .card-button:hover { background-color: var(--kyoto-color-hover); }
.santorini .card-badge { background-color: var(--santorini-color); }
.santorini .card-title, .santorini .price { color: var(--santorini-color); }
.santorini .card-button { background-color: var(--santorini-color); }
.santorini .card-button:hover { background-color: var(--santorini-color-hover); }
.banff .card-badge { background-color: var(--banff-color); }
.banff .card-title, .banff .price { color: var(--banff-color); }
.banff .card-button { background-color: var(--banff-color); }
.banff .card-button:hover { background-color: var(--banff-color-hover); }
.bali .card-badge { background-color: var(--bali-color); }
.bali .card-title, .bali .price { color: var(--bali-color); }
.bali .card-button { background-color: var(--bali-color); }
.bali .card-button:hover { background-color: var(--bali-color-hover); }
.marrakech .card-badge { background-color: var(--marrakech-color); }
.marrakech .card-title, .marrakech .price { color: var(--marrakech-color); }
.marrakech .card-button { background-color: var(--marrakech-color); }
.marrakech .card-button:hover { background-color: var(--marrakech-color-hover); }
.machupicchu .card-badge { background-color: var(--machupicchu-color); }
.machupicchu .card-title, .machupicchu .price { color: var(--machupicchu-color); }
.machupicchu .card-button { background-color: var(--machupicchu-color); }
.machupicchu .card-button:hover { background-color: var(--machupicchu-color-hover); }
/* Responsive styles */
@media (max-width: 767px) {
.card {
width: 260px;
}
.nav-button {
width: 45px;
height: 45px;
font-size: 1.2rem;
}
.prev-btn {
left: -15px;
}
.next-btn {
right: -15px;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment