scroll-mask
A collection of Tailwind CSS utilities for fading edges of scroll containers based on scroll position.
Uses animation-timeline: scroll() (see browser support) to animate mask-image without any JavaScript.
Installation
Add this CSS to your Tailwind CSS v4 stylesheet:
@property --scroll-mask-t-from {
syntax: "<length-percentage>";
inherits: false;
initial-value: 100%;
}
@property --scroll-mask-b-from {
syntax: "<length-percentage>";
inherits: false;
initial-value: 100%;
}
@property --scroll-mask-l-from {
syntax: "<length-percentage>";
inherits: false;
initial-value: 100%;
}
@property --scroll-mask-r-from {
syntax: "<length-percentage>";
inherits: false;
initial-value: 100%;
}
@keyframes scroll-mask-y-scroll {
0% {
--scroll-mask-t-from: 100%;
}
10%,
100% {
--scroll-mask-t-from: var(--scroll-mask-fade-from-t, 100%);
}
0%,
90% {
--scroll-mask-b-from: var(--scroll-mask-fade-from-b, 100%);
}
100% {
--scroll-mask-b-from: 100%;
}
}
@keyframes scroll-mask-x-scroll {
0% {
--scroll-mask-l-from: 100%;
}
10%,
100% {
--scroll-mask-l-from: var(--scroll-mask-fade-from-l, 100%);
}
0%,
90% {
--scroll-mask-r-from: var(--scroll-mask-fade-from-r, 100%);
}
100% {
--scroll-mask-r-from: 100%;
}
}
@supports (animation-timeline: scroll()) {
:where([class^="scroll-mask-"], [class*=" scroll-mask-"]) {
--scroll-mask-t: linear-gradient(
to top,
black,
black var(--scroll-mask-t-from),
transparent
);
--scroll-mask-b: linear-gradient(
to bottom,
black,
black var(--scroll-mask-b-from),
transparent
);
--scroll-mask-l: linear-gradient(
to left,
black,
black var(--scroll-mask-l-from),
transparent
);
--scroll-mask-r: linear-gradient(
to right,
black,
black var(--scroll-mask-r-from),
transparent
);
mask-image:
var(--scroll-mask-t), var(--scroll-mask-b), var(--scroll-mask-l),
var(--scroll-mask-r);
mask-composite: intersect;
-webkit-mask-composite: source-in;
animation:
scroll-mask-y-scroll linear,
scroll-mask-x-scroll linear;
animation-timeline: scroll(self block), scroll(self inline);
}
}
@utility scroll-mask-t {
--scroll-mask-fade-from-t: 80%;
}
@utility scroll-mask-t-from-* {
--scroll-mask-fade-from-t: --spacing(--value(integer));
--scroll-mask-fade-from-t: --value(percentage);
--scroll-mask-fade-from-t: --value([length], [percentage]);
}
@utility scroll-mask-b {
--scroll-mask-fade-from-b: 80%;
}
@utility scroll-mask-b-from-* {
--scroll-mask-fade-from-b: --spacing(--value(integer));
--scroll-mask-fade-from-b: --value(percentage);
--scroll-mask-fade-from-b: --value([length], [percentage]);
}
@utility scroll-mask-y {
--scroll-mask-fade-from-t: 80%;
--scroll-mask-fade-from-b: 80%;
}
@utility scroll-mask-y-from-* {
--scroll-mask-fade-from-t: --spacing(--value(integer));
--scroll-mask-fade-from-t: --value(percentage);
--scroll-mask-fade-from-t: --value([length], [percentage]);
--scroll-mask-fade-from-b: --spacing(--value(integer));
--scroll-mask-fade-from-b: --value(percentage);
--scroll-mask-fade-from-b: --value([length], [percentage]);
}
@utility scroll-mask-l {
--scroll-mask-fade-from-l: 80%;
}
@utility scroll-mask-l-from-* {
--scroll-mask-fade-from-l: --spacing(--value(integer));
--scroll-mask-fade-from-l: --value(percentage);
--scroll-mask-fade-from-l: --value([length], [percentage]);
}
@utility scroll-mask-r {
--scroll-mask-fade-from-r: 80%;
}
@utility scroll-mask-r-from-* {
--scroll-mask-fade-from-r: --spacing(--value(integer));
--scroll-mask-fade-from-r: --value(percentage);
--scroll-mask-fade-from-r: --value([length], [percentage]);
}
@utility scroll-mask-x {
--scroll-mask-fade-from-l: 80%;
--scroll-mask-fade-from-r: 80%;
}
@utility scroll-mask-x-from-* {
--scroll-mask-fade-from-l: --spacing(--value(integer));
--scroll-mask-fade-from-l: --value(percentage);
--scroll-mask-fade-from-l: --value([length], [percentage]);
--scroll-mask-fade-from-r: --spacing(--value(integer));
--scroll-mask-fade-from-r: --value(percentage);
--scroll-mask-fade-from-r: --value([length], [percentage]);
}
Axes
The axis utilities fade either end of the axis depending on scroll position: scroll-mask-y for vertical overflow, scroll-mask-x for horizontal.
- Ganymede01
- Titan02
- Callisto03
- Io04
- Luna05
- Europa06
- Triton07
- Titania08
- Oberon09
- Rhea10
- Iapetus11
- Charon12
- Umbriel13
- Ariel14
- Dione15
- Tethys16
- animation-timeline
- mask-image
- linear-gradient
- @property
- @keyframes
- @utility
- intersect
- composite
Masking edges
The four directional utilities fade a single edge.
- Ganymede01
- Titan02
- Callisto03
- Io04
- Luna05
- Europa06
- Triton07
- Titania08
- Oberon09
- Rhea10
- Iapetus11
- Charon12
- Umbriel13
- Ariel14
- Dione15
- Tethys16
- Ganymede01
- Titan02
- Callisto03
- Io04
- Luna05
- Europa06
- Triton07
- Titania08
- Oberon09
- Rhea10
- Iapetus11
- Charon12
- Umbriel13
- Ariel14
- Dione15
- Tethys16
- animation-timeline
- mask-image
- linear-gradient
- @property
- @keyframes
- @utility
- intersect
- composite
- animation-timeline
- mask-image
- linear-gradient
- @property
- @keyframes
- @utility
- intersect
- composite
Custom stops
The -from-* suffix mirrors Tailwind’s mask-* family. The value is the opaque-stop position, so from-90% fades the gradient from 90% to 100% near the masked edge (a 10% fade region).
- Ganymede01
- Titan02
- Callisto03
- Io04
- Luna05
- Europa06
- Triton07
- Titania08
- Oberon09
- Rhea10
- Iapetus11
- Charon12
- Umbriel13
- Ariel14
- Dione15
- Tethys16
- Ganymede01
- Titan02
- Callisto03
- Io04
- Luna05
- Europa06
- Triton07
- Titania08
- Oberon09
- Rhea10
- Iapetus11
- Charon12
- Umbriel13
- Ariel14
- Dione15
- Tethys16
- animation-timeline
- mask-image
- linear-gradient
- @property
- @keyframes
- @utility
- intersect
- composite
- animation-timeline
- mask-image
- linear-gradient
- @property
- @keyframes
- @utility
- intersect
- composite