Scroll Fades
A scroll container with a hard edge looks like a list that ends. A scroll fade softens that edge so the last visible row dissolves into the background, and the reader understands there is more below without a scrollbar telling them.
The two lists below share their scroll position. On the left the rows are cut off flat. On the right a mask fades them out over the last few pixels. Scroll either one.
- Review pull request #482
- Update onboarding copy
- Fix focus ring on the date picker
- Write release notes for 2.4
- Reply to the design review thread
- Migrate icons to the new set
- Check contrast on the dark theme
- Prepare the roadmap for Q4
- Book the offsite venue
- Archive the old marketing site
- Review pull request #482
- Update onboarding copy
- Fix focus ring on the date picker
- Write release notes for 2.4
- Reply to the design review thread
- Migrate icons to the new set
- Check contrast on the dark theme
- Prepare the roadmap for Q4
- Book the offsite venue
- Archive the old marketing site
The fade comes from mask-image with a linear gradient. Where the gradient is transparent the content is hidden, where it is black the content shows. Because it is a mask and not an overlay, it works over any background, including images and gradients, and it never blocks clicks.
Only fade the side that can scroll
A static mask has one flaw: it fades the top row even when you are at the top, and the last row even when you are at the bottom. At the bottom this is actively misleading. The final item looks half hidden, as if something is still below it.
Scroll both lists all the way down.
- Review pull request #482
- Update onboarding copy
- Fix focus ring on the date picker
- Write release notes for 2.4
- Reply to the design review thread
- Migrate icons to the new set
- Check contrast on the dark theme
- Prepare the roadmap for Q4
- Book the offsite venue
- Archive the old marketing site
- Review pull request #482
- Update onboarding copy
- Fix focus ring on the date picker
- Write release notes for 2.4
- Reply to the design review thread
- Migrate icons to the new set
- Check contrast on the dark theme
- Prepare the roadmap for Q4
- Book the offsite venue
- Archive the old marketing site
The right list measures its scroll position and switches each fade on only when there is content in that direction. The fade sizes live in two custom properties registered with @property, so the change animates over 200ms instead of popping.
Horizontal rows
The same idea applies sideways. A row of chips or tabs that overflows should fade at the edge that can still scroll, which also hints at the gesture.
Hide the scrollbar on rows like this. The fade is doing the scrollbar's job, and a horizontal bar under a row of chips is visual noise.
Usage
<!-- Vertical, static fade of 2.5rem on both ends -->
<div
class="h-44 overflow-y-auto [mask-image:linear-gradient(to_bottom,transparent,black_2.5rem,black_calc(100%-2.5rem),transparent)]"
>
...
</div>
<!-- Horizontal, and hide the scrollbar -->
<div
class="flex overflow-x-auto [scrollbar-width:none] [mask-image:linear-gradient(to_right,transparent,black_2rem,black_calc(100%-2rem),transparent)]"
>
...
</div>/* Static fade */
.list {
overflow-y: auto;
mask-image: linear-gradient(
to bottom,
transparent,
black 2.5rem,
black calc(100% - 2.5rem),
transparent
);
}
/* Edge aware fade. Update the two properties from a scroll listener. */
@property --fade-start {
syntax: "<length>";
inherits: false;
initial-value: 0px;
}
@property --fade-end {
syntax: "<length>";
inherits: false;
initial-value: 0px;
}
.list {
--fade-start: 0px;
--fade-end: 2.5rem;
mask-image: linear-gradient(
to bottom,
transparent,
black var(--fade-start),
black calc(100% - var(--fade-end)),
transparent
);
transition: --fade-start 200ms ease-out, --fade-end 200ms ease-out;
}This site ships a fade-mask-y utility in the global stylesheet. It uses a few extra color stops so the fade eases in rather than running linearly, which reads a little more naturally. For the edge aware version, listen to scroll and compare scrollTop + clientHeight with scrollHeight. Leave a pixel of tolerance, because those values are rounded differently on zoomed pages.
Resources
mask-image on MDN - The property that does the fading, with the gradient and image forms it accepts.
Building the Next.js website - Rauno Freiberg on the small layout details of a real site, including faded scroll edges.
@property on MDN - How to register a custom property so its value can be transitioned, which the edge aware demo relies on.
CSS.registerProperty() - The JavaScript version of @property, handy when the CSS lives inside a component.