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 www.npmjs.com favicon library before on another project, but fancied a challenge of doing it myself for my portfolio.

I came across this article htmldom.dev favicon , 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 github.com favicon for you to view. You can always contact me joebailey.xyz favicon if you need further help.