<div class="viewport">
<div class="carousel-frame">
<div class="carousel">
<ul class="scroll">
<li class="scroll-item-outer">
<div class="scroll-item">
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/7/75/Cute_grey_kitten.jpg/1280px-Cute_grey_kitten.jpg" alt="Picture of a gray kitten looking at the camera" />
</div>
</li>
<li class="scroll-item-outer">
<div class="scroll-item">
<img src="https://upload.wikimedia.org/wikipedia/commons/0/06/Kitten_in_Rizal_Park%2C_Manila.jpg" alt="Picture of a gray kitten looking at a branch"/>
</div>
</li>
<li class="scroll-item-outer">
<div class="scroll-item">
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/a/a5/Red_Kitten_01.jpg/1280px-Red_Kitten_01.jpg" alt="Picture of an orange kitten looking at the camera" />
</div>
</li>
<li class="scroll-item-outer">
<div class="scroll-item">
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/f/f3/Youngkitten.JPG/1280px-Youngkitten.JPG" alt="Picture of a young kitten opening its eyes"/>
</div>
</li>
</ul>
</div>
</div>
</div>
<ul class="indicators">
<li class="indicator">
<button class="indicator-button" aria-pressed="true"></button>
</li>
<li class="indicator">
<button class="indicator-button"></button>
</li>
<li class="indicator">
<button class="indicator-button"></button>
</li>
<li class="indicator">
<button class="indicator-button"></button>
</li>
</ul>
<footer style="margin: 20px; font-size: 0.8em;">All images are via <a href="https://commons.wikimedia.org">Wikimedia Commons</a> with a public domain or share-alike license.</footer>
:root {
--carousel-width: 40vw;
--carousel-height: calc(0.7 * var(--carousel-width));
--carousel-padding: 5px;
}
@media (max-width: 479px) {
:root {
--carousel-width: 95vw;
}
}
.viewport {
width: 100%;
margin: 0 auto;
display: flex;
flex-direction: column;
align-items: center;
}
.carousel-frame {
background: #fafafa;
padding: 10px;
border-radius: 3px;
border: 1px solid #ddd;
width: calc(var(--carousel-width) + (2 * var(--carousel-padding)));
height: calc(var(--carousel-height) + (2 * var(--carousel-padding)));
}
.carousel {
width: var(--carousel-width);
height: var(--carousel-height);
}
.scroll {
display: flex;
align-items: center;
overflow-x: auto;
overflow-y: hidden;
width: 100%;
height: 100%;
-webkit-overflow-scrolling: touch;
}
ul.scroll {
margin: 0;
padding: 0;
list-style: none;
}
.scroll-item-outer {
width: 100%;
height: 100%
}
.scroll-item {
width: var(--carousel-width);
height: 100%;
}
img {
object-fit: contain;
width: 100%;
height: 100%;
}
@supports (scroll-snap-align: start) {
/* modern scroll snap points */
.scroll {
scroll-snap-type: x mandatory;
}
.scroll-item-outer {
scroll-snap-align: center;
}
}
@supports not (scroll-snap-align: start) {
/* old scroll snap points spec */
.scroll {
-webkit-scroll-snap-type: mandatory;
scroll-snap-type: mandatory;
-webkit-scroll-snap-destination: 0% center;
scroll-snap-destination: 0% center;
-webkit-scroll-snap-points-x: repeat(100%);
scroll-snap-points-x: repeat(100%);
}
.scroll-item-outer {
scroll-snap-coordinate: 0 0;
}
}
.indicators {
display: flex;
width: 100%;
justify-content: center;
}
ul.indicators {
margin: 0;
padding: 0;
list-style: none;
}
.indicator {
padding: 10px;
}
.indicator-button {
cursor: pointer;
background: none;
border: none;
color: #333;
overflow: hidden;
display: flex;
flex-direction: column;
padding: 0;
}
.indicator-button:after {
content: '○';
font-size: 1.4em;
padding: 12px 15px 17px;
}
.indicator-button:hover {
color: #666
}
.indicator-button:active {
color: #999;
padding: 0;
}
.indicator-button[aria-pressed="true"]:after {
content: '●';
}
// via https://github.com/tootsuite/mastodon/blob/f59ed3a4fafab776b4eeb92f805dfe1fecc17ee3/app/javascript/mastodon/scroll.js
const easingOutQuint = (x, t, b, c, d) =>
c * ((t = t / d - 1) * t * t * t * t + 1) + b
function smoothScrollPolyfill (node, key, target) {
const startTime = Date.now()
const offset = node[key]
const gap = target - offset
const duration = 1000
let interrupt = false
const step = () => {
const elapsed = Date.now() - startTime
const percentage = elapsed / duration
if (interrupt) {
return
}
if (percentage > 1) {
cleanup()
return
}
node[key] = easingOutQuint(0, elapsed, offset, gap, duration)
requestAnimationFrame(step)
}
const cancel = () => {
interrupt = true
cleanup()
}
const cleanup = () => {
node.removeEventListener('wheel', cancel)
node.removeEventListener('touchstart', cancel)
}
node.addEventListener('wheel', cancel, { passive: true })
node.addEventListener('touchstart', cancel, { passive: true })
step()
return cancel
}
function testSupportsSmoothScroll () {
let supports = false
try {
let div = document.createElement('div')
div.scrollTo({
top: 0,
get behavior () {
supports = true
return 'smooth'
}
})
} catch (err) {} // Edge throws an error
return supports
}
const hasNativeSmoothScroll = testSupportsSmoothScroll()
function smoothScroll (node, topOrLeft, horizontal) {
if (hasNativeSmoothScroll) {
return node.scrollTo({
[horizontal ? 'left' : 'top']: topOrLeft,
behavior: 'smooth'
})
} else {
return smoothScrollPolyfill(node, horizontal ? 'scrollLeft' : 'scrollTop', topOrLeft)
}
}
function debounce(func, ms) {
let timeout
return () => {
clearTimeout(timeout)
timeout = setTimeout(() => {
timeout = null
func()
}, ms)
}
}
const indicators = document.querySelectorAll('.indicator-button')
const scroller = document.querySelector('.scroll')
function setAriaLabels() {
indicators.forEach((indicator, i) => {
indicator.setAttribute('aria-label', `Scroll to item #${i + 1}`)
})
}
function setAriaPressed(index) {
indicators.forEach((indicator, i) => {
indicator.setAttribute('aria-pressed', !!(i === index))
})
}
indicators.forEach((indicator, i) => {
indicator.addEventListener('click', e => {
e.preventDefault()
e.stopPropagation()
setAriaPressed(i)
const scrollLeft = Math.floor(scroller.scrollWidth * (i / 4))
smoothScroll(scroller, scrollLeft, true)
})
})
scroller.addEventListener('scroll', debounce(() => {
let index = Math.round((scroller.scrollLeft / scroller.scrollWidth) * 4)
setAriaPressed(index)
}, 200))
setAriaLabels()
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.