A Pen by Gemma Croad on CodePen.
Created
April 29, 2025 23:45
-
-
Save hackur/91a9336a0f03b97d52c14e477bf7175c to your computer and use it in GitHub Desktop.
CodePen Challenge: Card Carousel
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
}); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
: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