A Pen by Taha Shashtari on CodePen.
Created
July 20, 2025 19:22
-
-
Save rhukster/06ed02eed1b9f4305a51f44a567f7d3c to your computer and use it in GitHub Desktop.
Infinite 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
<div class="infinite-carousel"> | |
<div | |
class="item-container" | |
data-vel-plugin="InfiniteCarousel" | |
data-vel-view="container" | |
data-vel-data-current-index="0" | |
> | |
<div class="item" data-vel-view="item"> | |
<div class="item-content"> | |
<img src="https://github.com/TahaSh/veloxi-infinite-carousel/blob/main/public/inside_out_2.jpg?raw=true" alt="" /> | |
<div class="content"> | |
<h2 class="title">Inside Out 2</h2> | |
</div> | |
</div> | |
</div> | |
<div class="item" data-vel-view="item"> | |
<div class="item-content"> | |
<img src="https://github.com/TahaSh/veloxi-infinite-carousel/blob/main/public/elemental.jpg?raw=true" alt="" /> | |
<div class="content"> | |
<h2 class="title">Elemental</h2> | |
</div> | |
</div> | |
</div> | |
<div class="item" data-vel-view="item"> | |
<div class="item-content"> | |
<img src="https://github.com/TahaSh/veloxi-infinite-carousel/blob/main/public/turning_red.jpg?raw=true" alt="" /> | |
<div class="content"> | |
<h2 class="title">Turning Red</h2> | |
</div> | |
</div> | |
</div> | |
<div class="item" data-vel-view="item"> | |
<div class="item-content"> | |
<img src="https://github.com/TahaSh/veloxi-infinite-carousel/blob/main/public/luca.jpg?raw=true" alt="" /> | |
<div class="content"> | |
<h2 class="title">Luca</h2> | |
</div> | |
</div> | |
</div> | |
<div class="item" data-vel-view="item"> | |
<div class="item-content"> | |
<img src="https://github.com/TahaSh/veloxi-infinite-carousel/blob/main/public/toy_story_4.jpg?raw=true" alt="" /> | |
<div class="content"> | |
<h2 class="title">Toy Story 4</h2> | |
</div> | |
</div> | |
</div> | |
</div> | |
</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
// Created with Veloxi: https://veloxijs.com/guides/introduction/ | |
const { DragEvent, DragEventPlugin, Utils, createApp } = Veloxi | |
const MIN_OPACITY = 0.35 | |
export class NextEvent {} | |
export class PreviousEvent {} | |
export const InfiniteCarouselPlugin = (context) => { | |
const dragEventPlugin = context.useEventPlugin(DragEventPlugin) | |
dragEventPlugin.on(DragEvent, onDrag) | |
let draggingWidth = 0 | |
let totalDragging = 0 | |
let container | |
let items | |
let totalItems | |
let containerX | |
let startDraggingX = 0 | |
function getCurrentIndex() { | |
return parseInt(container.data.currentIndex) | |
} | |
context.onDataChanged((data) => { | |
if (data.dataName === 'currentIndex') { | |
const isDragging = startDraggingX !== 0 | |
if (isDragging) { | |
updateItems(false) | |
} else { | |
update() | |
} | |
} | |
}) | |
function itemWidth() { | |
return container.size.width | |
} | |
function updateOpacity() { | |
const initialX = container.position.initialX | |
items.forEach((item) => { | |
const progress = Utils.pointToViewProgress( | |
{ x: initialX }, | |
item, | |
itemWidth() | |
) | |
const opacity = Utils.remap(progress, 0, 1, MIN_OPACITY, 1) | |
item.opacity.set(opacity) | |
}) | |
} | |
function onClick(item) { | |
const clickedIndex = items.indexOf(item) | |
if (clickedIndex === nextIndex()) { | |
context.emit(NextEvent, {}) | |
} else if (clickedIndex === previousIndex()) { | |
context.emit(PreviousEvent, {}) | |
} | |
} | |
function onDrag(event) { | |
if (event.isDragging) { | |
if (!startDraggingX) { | |
startDraggingX = containerX | |
} | |
container.position.set({ x: startDraggingX + event.width }, false) | |
draggingWidth = Math.abs(event.width) - totalDragging | |
updateOpacity() | |
} else { | |
if (event.width === 0) { | |
onClick(event.view) | |
} | |
startDraggingX = 0 | |
totalDragging = 0 | |
draggingWidth = 0 | |
update() | |
if (Math.abs(event.x - event.previousX) > 5) { | |
if (event.x < event.previousX) { | |
context.emit(NextEvent, {}) | |
} else if (event.x > event.previousX) { | |
context.emit(PreviousEvent, {}) | |
} | |
} | |
} | |
const threshold = (container.size.width * 2) / 3 | |
if (draggingWidth >= threshold) { | |
totalDragging = Math.abs(event.width) + container.size.width / 3 | |
draggingWidth = container.size.width / 3 | |
if (event.directions.includes('left')) { | |
context.emit(NextEvent, {}) | |
} else if (event.directions.includes('right')) { | |
context.emit(PreviousEvent, {}) | |
} | |
} else if (draggingWidth < -threshold) { | |
totalDragging = Math.abs(event.width) - container.size.width / 3 | |
draggingWidth = -container.size.width / 3 | |
if (event.directions.includes('left')) { | |
context.emit(NextEvent, {}) | |
} else if (event.directions.includes('right')) { | |
context.emit(PreviousEvent, {}) | |
} | |
} | |
} | |
context.setup(() => { | |
container = context.getView('container') | |
container.position.setAnimator('spring', { damping: 0.64, stiffness: 0.7 }) | |
items = context.getViews('item') | |
items.forEach((item) => { | |
item.opacity.setAnimator('dynamic') | |
dragEventPlugin.addView(item) | |
}) | |
totalItems = items.length | |
update() | |
window.addEventListener('resize', update) | |
}) | |
function update() { | |
updateContainerPosition() | |
updateItems() | |
} | |
function updateContainerPosition() { | |
const initialX = container.position.initialX | |
containerX = initialX - getCurrentIndex() * itemWidth() | |
container.position.set({ x: containerX }) | |
} | |
function localCurrentIndex() { | |
return ((getCurrentIndex() % totalItems) + totalItems) % totalItems | |
} | |
function nextIndex() { | |
return (localCurrentIndex() + 1) % totalItems | |
} | |
function previousIndex() { | |
return (localCurrentIndex() - 1 + totalItems) % totalItems | |
} | |
function updateItems(updateOpacity = true) { | |
const currentIndex = getCurrentIndex() | |
const totalWidth = itemWidth() * totalItems | |
const segment = Math.floor(currentIndex / totalItems) | |
const baseX = container.position.initialX + segment * totalWidth | |
const currentIndexPosition = baseX + localCurrentIndex() * itemWidth() | |
items.forEach((item, index) => { | |
if (updateOpacity) { | |
if (index === localCurrentIndex()) { | |
item.opacity.set(1, context.initialized) | |
} else { | |
item.opacity.set(MIN_OPACITY, context.initialized) | |
} | |
} | |
if (index === previousIndex()) { | |
item.position.set({ x: currentIndexPosition - itemWidth() }) | |
} else if (index === nextIndex()) { | |
item.position.set({ x: currentIndexPosition + itemWidth() }) | |
} else if (!context.initialized || index === localCurrentIndex()) { | |
item.position.set({ x: baseX + index * itemWidth() }) | |
} | |
}) | |
} | |
} | |
InfiniteCarouselPlugin.pluginName = 'InfiniteCarousel' | |
const app = createApp() | |
app.addPlugin(InfiniteCarouselPlugin) | |
app.run() | |
const container = document.querySelector( | |
'.infinite-carousel .item-container' | |
) | |
app.onPluginEvent(InfiniteCarouselPlugin, PreviousEvent, () => { | |
const currentIndex = parseInt(container.dataset.velDataCurrentIndex) | |
container.dataset.velDataCurrentIndex = `${currentIndex - 1}` | |
}) | |
app.onPluginEvent(InfiniteCarouselPlugin, NextEvent, () => { | |
const currentIndex = parseInt(container.dataset.velDataCurrentIndex) | |
container.dataset.velDataCurrentIndex = `${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
<script src="https://unpkg.com/[email protected]/dist/veloxi.min.js"></script> |
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 { | |
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; | |
line-height: 1.5; | |
font-weight: 400; | |
color-scheme: light dark; | |
color: rgba(255, 255, 255, 0.87); | |
background-color: #f0f0f0; | |
font-synthesis: none; | |
text-rendering: optimizeLegibility; | |
-webkit-font-smoothing: antialiased; | |
-moz-osx-font-smoothing: grayscale; | |
} | |
body { | |
margin: 0; | |
padding: 0; | |
width: 100%; | |
min-height: 100dvh; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
} | |
.infinite-carousel { | |
width: 100%; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
overflow: hidden; | |
} | |
.item-container { | |
width: 80%; | |
aspect-ratio: 0.5; | |
max-height: 95vh; | |
position: relative; | |
touch-action: none; | |
} | |
.item { | |
width: 100%; | |
height: 100%; | |
display: flex; | |
justify-content: center; | |
position: absolute; | |
user-select: none; | |
cursor: pointer; | |
} | |
.item-content { | |
width: calc(100% - 20px); | |
border-radius: 10px; | |
height: 100%; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
overflow: hidden; | |
position: relative; | |
background: #e0e0e0; | |
} | |
.item-content img { | |
width: 100%; | |
height: 100%; | |
object-fit: cover; | |
pointer-events: none; | |
z-index: 1; | |
} | |
.item-content .content { | |
position: absolute; | |
display: flex; | |
align-items: flex-end; | |
justify-content: center; | |
width: 100%; | |
height: 100%; | |
z-index: 3; | |
} | |
.item-content .content::before { | |
content: ''; | |
position: absolute; | |
z-index: 2; | |
width: 100%; | |
height: 50%; | |
background: linear-gradient(transparent, #000); | |
} | |
.item-content .title { | |
font-size: 6vw; | |
z-index: 4; | |
padding-bottom: 40px; | |
margin: 0; | |
user-select: none; | |
-webkit-user-select: none; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment