I have an example of my current work here: https://jsfiddle.net/pv5xroLc/
My problem is that when the table in my example is fully scrolled to the right, the faded gradient still covers part of my table even though it cannot be scrolled further, thus it makes the last column harder to read. I am wondering what the best solution to hiding this gradient is while still making it clear that the table can be scrolled horizontally (this is going to appear on mobile).
Currently, my html structure is as follows:
<div class="fader">
<div class="scrollable">
*content*
</div>
</div>
The .fader
element has an ::after
pseudo-element which contains the "fader" on it, which is an absolutely positioned element with the linear-gradient I'm using to indicate that the element can be scrolled horizontally. The .scrollable
element is a horizontally scrolling element that holds my table.
I currently have two solutions I have considered:
.scrollable
element but it does not add any extra space after the table.If anyone has suggestions for what they think I should do, it would be greatly appreciated.
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:
ref
property is added to the .scrollable
div so that it can be easily referenced in the component's script.onScroll
method is attached to the scroll event of the scrollable
ref when the component is mounted
and removed when the component is destroyed
.onScroll
method calls an updateIsScrolledRight
method, passing it the scroll event's target
(the .scrollable
div).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.:class
attribute which will add the scrolled-right
class to the div if the value of isScrolledRight
is true
..scrolled-right
class sets the div's ::after
content to have opacity: 0;
.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>
(I'd rather comment on the OP than answer, but it won't let me start a comment yet because I'm too new so apologies for that.)
I've dealt with stuff like this before, and my quick hack following your #2 solution idea above would be to add another <th>
after <th>Year End</th>
, then style that with the appropriate width to match the gradient fade. You could then decide whether to also put in blank <td>
s below it.
ALSO I noticed that your white gradient has a pretty solid white line where it starts (right above "$9") - you can smooth that out in line 15/16 of your SCSS by adding more reference points (it took me a while to figure this out back in the day, just including in case it's helpful):
background: linear-gradient(to right, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.2), white);
width: 30%;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With