My portfolio has some cards to showcase projects and blog posts. On mobile, these cards display in a horizontal slider, which is easy enough to scroll on a touchscreen, or trackpad, but what if someone is viewing the website at a small size on a device with a mouse? Well they can of course use the circular buttons below the cards, but I wanted to give these users an experience the same as on a touchscreen, allowing them to drag and scroll the card list.
I’ve used the vue-dragscroll library before on another project, but fancied a challenge of doing it myself for my portfolio.
I came across this article, and adapted the code to fit my use-case.
The code
.scroll-container {
display: grid;
column-gap: 10px;
grid-auto-flow: column;
// We set the grid colums here, a gutter each side, then I have 6 cards so I use the grid repeat function to make 6 equal width columns. The columns are 100vw minus the left and right gutter, and minus the column gap we set above
grid-template-columns: 30px repeat(6, calc(100vw - 80px)) 30px;
// We want to allow the cards to overflow horizontally
overflow-x: auto;
padding: 0;
// This allows snapping to each card, so we don't get stuck half over one card and half over another. https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-snap-type
scroll-snap-type: x mandatory;
}
.drag-scroll--enabled {
cursor: grab
}
.drag-scroll--scrolling {
cursor: grabbing;
user-select: none;
// We set the scroll-snap-type to none here to allow for a more natural experience with dragging and scrolling. If we didn't, you wouldn't see any indication that you are scrolling the container
scroll-snap-type: none
}
<div ref="container" class="row grid scroll-container">
<div class="card">
...
</div>
</div>
export default {
data () {
return {
position: {
left: 0,
x: 0
}
}
},
mounted () {
this.dragScrollWatcher()
// We want to listen to the resize listener here to enable/disable the drag to scroll functionality depending on the layout of the page - for example, on my site, the cards are only in a horizontal slider below the 768px breakpoint. I chose to handle this with CSS in case I want to use these functions elsewhere, rather than having these breakpoints set in the JS
window.addEventListener('resize', this.dragScrollWatcher)
},
beforeDestroy () {
// We want to clear up any event listeners when we switch pages
this.stopDragScroll()
window.removeEventListener('resize', this.dragScrollWatcher)
},
methods: {
dragScrollWatcher () {
// We only want to start drag scroll if the following conditions are met
if (!this.hasTouchScreen() && this.hasOverflowAuto()) {
this.startDragScroll()
} else {
this.stopDragScroll()
}
},
startDragScroll () {
// We set a listener for mousedcown so we know when to start the drag and scroll
document.addEventListener('mousedown', this.mouseDownHandler)
//
We set this class on the container to allow the CSS to set some styles such as the cursor: grab
this.$refs.container.classList.add('drag-scroll--enabled')
},
stopDragScroll () {
document.removeEventListener('mousedown', this.mouseDownHandler)
this.$refs.container.classList.remove('drag-scroll--enabled')
// This clears up some event listeners and resets our classes
this.mouseUpHandler()
},
hasTouchScreen () {
// If this is a touch device, scrolling is already easy, so we don't need to enable our drag scroll feature
return ('ontouchstart' in window)
},
hasOverflowAuto () {
/*
Rather than worrying about breakpoints here, we let CSS handle it, as they may be different for each component
If overflow-x: auto is not on the element, then it is not a scrolling element, so we don't need to run DragToScroll
*/
return (getComputedStyle(this.$refs.container).getPropertyValue('overflow-x') === 'auto')
},
mouseDownHandler (e) {
// We set a class here to let the CSS know that we are currently scrolling, and to apply the relevant styles, such as the grabbing cursor
this.$refs.container.classList.add('drag-scroll--scrolling')
this.position = {
// The current scroll
left: this.$refs.container.scrollLeft,
// Get the current mouse position
x: e.clientX
}
// We want to listen to the mouse move so we know how much to scroll the container
document.addEventListener('mousemove', this.mouseMoveHandler)
// We want to know when to stop dragging and scrolling
document.addEventListener('mouseup', this.mouseUpHandler)
},
mouseMoveHandler (e) {
// How far the mouse has been moved
const dx = e.clientX - this.position.x
// Scroll the element
this.$refs.container.scrollLeft = this.position.left - dx
},
mouseUpHandler () {
// We don't care about listening to the mouse moving now, so we can remove the listener
document.removeEventListener('mousemove', this.mouseMoveHandler)
// We've just fired this listener, so no need to fire it again
document.removeEventListener('mouseup', this.mouseUpHandler)
// We can now remove the class which means we don't show the styles specific to when we are scrolling
this.$refs.container.classList.remove('drag-scroll--scrolling')
}
}
}
How it looks
Native touch scroll
Dragging to Scroll with JavaScript
I often find articles and need some further context before I can adapt them, so all of the source code of my site is available on GitHub for you to view. You can always contact me if you need further help.