Skip to content

Instantly share code, notes, and snippets.

@rhukster
Created July 20, 2025 19:22
Show Gist options
  • Save rhukster/06ed02eed1b9f4305a51f44a567f7d3c to your computer and use it in GitHub Desktop.
Save rhukster/06ed02eed1b9f4305a51f44a567f7d3c to your computer and use it in GitHub Desktop.
Infinite Carousel
<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>
// 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}`
})
<script src="https://unpkg.com/[email protected]/dist/veloxi.min.js"></script>
: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