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-type12 scroll-snap-type: x mandatory;13}14.drag-scroll--enabled {15 cursor: grab16}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 container21 scroll-snap-type: none22}
1<div ref="container" class="row grid scroll-container">2 <div class="card">3 ...4 </div>5</div>
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 JS13 window.addEventListener('resize', this.dragScrollWatcher)14 },15 beforeDestroy () {16 // We want to clear up any event listeners when we switch pages17 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 met24 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 scroll33 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: grab36 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 classes42 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 feature47 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 component52 If overflow-x: auto is not on the element, then it is not a scrolling element, so we don't need to run DragToScroll53 */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 cursor59 this.$refs.container.classList.add('drag-scroll--scrolling')60 61 this.position = {62 // The current scroll63 left: this.$refs.container.scrollLeft,64 // Get the current mouse position65 x: e.clientX66 }67 68 // We want to listen to the mouse move so we know how much to scroll the container69 document.addEventListener('mousemove', this.mouseMoveHandler)70 71 // We want to know when to stop dragging and scrolling72 document.addEventListener('mouseup', this.mouseUpHandler)73 },74 mouseMoveHandler (e) {75 // How far the mouse has been moved76 const dx = e.clientX - this.position.x77 78 // Scroll the element79 this.$refs.container.scrollLeft = this.position.left - dx80 },81 mouseUpHandler () {82 83 // We don't care about listening to the mouse moving now, so we can remove the listener84 document.removeEventListener('mousemove', this.mouseMoveHandler)85 // We've just fired this listener, so no need to fire it again86 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 scrolling89 this.$refs.container.classList.remove('drag-scroll--scrolling')90 }91 }92}
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.