twilson.net

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
scroll-mask-y
  • animation-timeline
  • mask-image
  • linear-gradient
  • @property
  • @keyframes
  • @utility
  • intersect
  • composite
scroll-mask-x

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
scroll-mask-t
  • Ganymede01
  • Titan02
  • Callisto03
  • Io04
  • Luna05
  • Europa06
  • Triton07
  • Titania08
  • Oberon09
  • Rhea10
  • Iapetus11
  • Charon12
  • Umbriel13
  • Ariel14
  • Dione15
  • Tethys16
scroll-mask-b
  • animation-timeline
  • mask-image
  • linear-gradient
  • @property
  • @keyframes
  • @utility
  • intersect
  • composite
scroll-mask-l
  • animation-timeline
  • mask-image
  • linear-gradient
  • @property
  • @keyframes
  • @utility
  • intersect
  • composite
scroll-mask-r

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
scroll-mask-b-from-80%
  • Ganymede01
  • Titan02
  • Callisto03
  • Io04
  • Luna05
  • Europa06
  • Triton07
  • Titania08
  • Oberon09
  • Rhea10
  • Iapetus11
  • Charon12
  • Umbriel13
  • Ariel14
  • Dione15
  • Tethys16
scroll-mask-t-from-95%
  • animation-timeline
  • mask-image
  • linear-gradient
  • @property
  • @keyframes
  • @utility
  • intersect
  • composite
scroll-mask-x-from-90%
  • animation-timeline
  • mask-image
  • linear-gradient
  • @property
  • @keyframes
  • @utility
  • intersect
  • composite
scroll-mask-r-from-[calc(100%-2rem)]