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

1.scroll-container {
2 display: grid;
3 column-gap: 10px;
4 grid-auto-flow: column;
5 // 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
6 grid-template-columns: 30px repeat(6, calc(100vw - 80px)) 30px;
7 // We want to allow the cards to overflow horizontally
8 overflow-x: auto;
9 padding: 0;
10 
11 // 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
12 scroll-snap-type: x mandatory;
13}
14.drag-scroll--enabled {
15 cursor: grab
16}
17.drag-scroll--scrolling {
18 cursor: grabbing;
19 user-select: none;
20 // 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
21 scroll-snap-type: none
22}
Copied!
1<div ref="container" class="row grid scroll-container">
2 <div class="card">
3 ...
4 </div>
5</div>
Copied!
1export default {
2 data () {
3 return {
4 position: {
5 left: 0,
6 x: 0
7 }
8 }
9 },
10 mounted () {
11 this.dragScrollWatcher()
12 // 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
13 window.addEventListener('resize', this.dragScrollWatcher)
14 },
15 beforeDestroy () {
16 // We want to clear up any event listeners when we switch pages
17 this.stopDragScroll()
18 window.removeEventListener('resize', this.dragScrollWatcher)
19 },
20methods: {
21 dragScrollWatcher () {
22 
23 // We only want to start drag scroll if the following conditions are met
24 if (!this.hasTouchScreen() && this.hasOverflowAuto()) {
25 this.startDragScroll()
26 } else {
27 this.stopDragScroll()
28 }
29 },
30 startDragScroll () {
31 
32 // We set a listener for mousedcown so we know when to start the drag and scroll
33 document.addEventListener('mousedown', this.mouseDownHandler)
34 //
35 We set this class on the container to allow the CSS to set some styles such as the cursor: grab
36 this.$refs.container.classList.add('drag-scroll--enabled')
37 },
38 stopDragScroll () {
39 document.removeEventListener('mousedown', this.mouseDownHandler)
40 this.$refs.container.classList.remove('drag-scroll--enabled')
41 // This clears up some event listeners and resets our classes
42 this.mouseUpHandler()
43 },
44 hasTouchScreen () {
45 
46 // If this is a touch device, scrolling is already easy, so we don't need to enable our drag scroll feature
47 return ('ontouchstart' in window)
48 },
49 hasOverflowAuto () {
50 /*
51 Rather than worrying about breakpoints here, we let CSS handle it, as they may be different for each component
52 If overflow-x: auto is not on the element, then it is not a scrolling element, so we don't need to run DragToScroll
53 */
54 return (getComputedStyle(this.$refs.container).getPropertyValue('overflow-x') === 'auto')
55 },
56 mouseDownHandler (e) {
57 
58 // 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
59 this.$refs.container.classList.add('drag-scroll--scrolling')
60 
61 this.position = {
62 // The current scroll
63 left: this.$refs.container.scrollLeft,
64 // Get the current mouse position
65 x: e.clientX
66 }
67 
68 // We want to listen to the mouse move so we know how much to scroll the container
69 document.addEventListener('mousemove', this.mouseMoveHandler)
70 
71 // We want to know when to stop dragging and scrolling
72 document.addEventListener('mouseup', this.mouseUpHandler)
73 },
74 mouseMoveHandler (e) {
75 // How far the mouse has been moved
76 const dx = e.clientX - this.position.x
77 
78 // Scroll the element
79 this.$refs.container.scrollLeft = this.position.left - dx
80 },
81 mouseUpHandler () {
82 
83 // We don't care about listening to the mouse moving now, so we can remove the listener
84 document.removeEventListener('mousemove', this.mouseMoveHandler)
85 // We've just fired this listener, so no need to fire it again
86 document.removeEventListener('mouseup', this.mouseUpHandler)
87 
88 // We can now remove the class which means we don't show the styles specific to when we are scrolling
89 this.$refs.container.classList.remove('drag-scroll--scrolling')
90 }
91 }
92}
Copied!

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.