Best approach to hide an absolutely positioned <div> when horizontally scrolling?

If I needed to implement this functionality, I would make a Vue component that would take in the table content (or any content) in a slot and then listen to the scroll event of the .scrollable div, adding or removing the faded ::after content if the div was scrolled all the way to the right.

Here's an example:

Vue.component('fader', {
  template: `
    <div class="fader" :class="{ 'scrolled-right': isScrolledRight }">
      <div class="scrollable" ref="scrollable">
        <slot></slot>
      </div>
    </div>
  `,
  data() {
    return {
      isScrolledRight: false,
    }
  },
  methods: {
    onScroll(event) {
      this.updateIsScrolledRight(event.target);
    },
    updateIsScrolledRight({ scrollLeft, offsetWidth, scrollWidth }) {
      this.isScrolledRight = (scrollLeft + offsetWidth) === scrollWidth;
    }
  },
  mounted() {
    this.$refs.scrollable.addEventListener('scroll', this.onScroll);
    this.updateIsScrolledRight(this.$refs.scrollable);
  },
  destroyed() {
    this.$refs.scrollable.removeEventListeneer('scroll', this.onScroll);
  }
}) 

.fader.scrolled-right::after {
  opacity: 0;
}

Here's how the component works:

  • A ref property is added to the .scrollable div so that it can be easily referenced in the component's script.
  • An onScroll method is attached to the scroll event of the scrollable ref when the component is mounted and removed when the component is destroyed.
  • The onScroll method calls an updateIsScrolledRight method, passing it the scroll event's target (the .scrollable div).
  • The updateIsScrolledRight method looks at the scrollLeft, offsetWidth, and scrollWidth properties of the element passed as the parameter to determine if the element is scrolled all the way to the right and sets an isScrolledRight property to true if so and false if not.
  • The root div of the component has a bound :class attribute which will add the scrolled-right class to the div if the value of isScrolledRight is true.
  • The .scrolled-right class sets the div's ::after content to have opacity: 0;.
  • The updateIsScrolledRight method is also called in the mounted hook so that, if the content in the <slot> happens to not be wide enough to need a scrollbar, the fade will be removed in that case as well.

Here's a full working example:

Vue.component('fader', {
  template: `
    <div class="fader" :class="{ 'scrolled-right': isScrolledRight }">
      <div class="scrollable" ref="scrollable">
        <slot></slot>
      </div>
    </div>
  `,
  data() {
    return {
      isScrolledRight: false,
    }
  },
  methods: {
    onScroll(event) { 
      this.updateIsScrolledRight(event.target);
    },
    updateIsScrolledRight({ scrollLeft, offsetWidth, scrollWidth }) {
      this.isScrolledRight = (scrollLeft + offsetWidth) === scrollWidth;
    }
  },
  mounted() {
    this.$refs.scrollable.addEventListener('scroll', this.onScroll);
    this.updateIsScrolledRight(this.$refs.scrollable);
  },
  destroyed() {
    this.$refs.scrollable.removeEventListeneer('scroll', this.onScroll);
  }
})

new Vue({
  el: "#app",
})
.fader {
  position: relative;
  width: 90%;
  margin-left: 46px;
}

.fader::after {
  content: "";
  position: absolute;
  z-index: 1;
  top: 0;
  right: -1px;
  bottom: 15px;
  pointer-events: none;
  background: linear-gradient(to right, rgba(255, 255, 255, 0.1), white);
  width: 10%;
  opacity: 1;
  transition: opacity .2s ease-out;
}

.fader .scrollable {
  white-space: nowrap;
  overflow-x: scroll;
  position: relative;
}
  
.fader.scrolled-right::after {
  opacity: 0;
}

.breakdown-title {
  font-size: 14px;
  font-weight: 700;
  text-align: center;
  margin: 8px auto;
}

table {
  font-size: 12px;
  margin: auto;
  color: #000;
  width: 100%;
  table-layout: fixed;
}

table thead {
  color: #fff;
  background-color: #da291c;
}

table thead th {
  width: 75px;
  text-align: right;
}

table thead th:first-of-type {
  width: 120px;
  padding-left: 4px;
}

table thead th:last-of-type {
  width: 80px;
  padding-right: 4px;
}

table tbody tr:nth-of-type(odd) {
  background-color: #fce9e8;
}

table tbody td {
  width: 75px;
  text-align: right;
}
 
table tbody td:first-of-type {
  width: 120px;
  text-align: left;
  padding-left: 4px;
}

table tbody td:last-of-type {
  width: 80px;
  padding-right: 4px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
  <fader>
    <div class="breakdown-title">Total Revenue Bonus</div>
    <table>
      <thead>
        <tr>
          <th></th>
          <th>Oct</th>
          <th>Nov</th>
          <th>Dec</th>
          <th>Jan</th>
          <th>Feb</th>
          <th>Mar</th>
          <th>Apr</th>
          <th>May</th>
          <th>Jun</th>
          <th>Jul</th>
          <th>Aug</th>
          <th>Sep</th>
          <th>Year End</th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>YTD Target</td>
          <td>$1,325,705</td>
          <td>$2,651,410</td>
          <td>$3,977,115</td>
          <td>$5,302,821</td>
          <td>$6,628,526</td>
          <td>$7,954,231</td>
          <td>$9,279,936</td>
          <td>$10,605,642</td>
          <td>$11,931,347</td>
          <td>$13,257,052</td>
          <td>$14,582,757</td>
          <td>$15,908,463</td>
          <td>$15,908,463</td>
        </tr>
        <tr>
          <td>YTD Actual</td>
          <td>$19,956</td>
          <td>$19,956</td>
          <td>$19,956</td>
          <td>$19,956</td>
          <td>$19,956</td>
          <td>$19,956</td>
          <td>$19,956</td>
          <td>$19,956</td>
          <td>$19,956</td>
          <td>$19,956</td>
          <td>$19,956</td>
          <td>$19,956</td>
          <td>$19,956</td>
        </tr>
        <tr>
          <td>% to Target</td>
          <td>2%</td>
          <td>1%</td>
          <td>1%</td>
          <td>0%</td>
          <td>0%</td>
          <td>0%</td>
          <td>0%</td>
          <td>0%</td>
          <td>0%</td>
          <td>0%</td>
          <td>0%</td>
          <td>0%</td>
          <td>0%</td>
        </tr>
      </tbody>
    </table>
  </fader>
</div>