Mask Gradient Y

Fades its child out at the top and at the bottom, so content scrolls into transparency instead of stopping against a hard edge.

API

Props

proptypedefault
children?ReactElementthe element to mask; cloned, not wrapped
fade?string"2rem"length of both fades at once
fadeTop?stringlength of the top fade; overrides fade
fadeBottom?stringlength of the bottom fade; overrides fade
offsetTop?string"0px"where the top fade starts
offsetBottom?string"0px"where the bottom fade starts, from the end

Examples

Usage

tsx
<MaskGradientY fade="3rem">
  <div className="h-80 overflow-y-auto"></div>
</MaskGradientY>
previewscroll the list

row 1

row 2

row 3

row 4

row 5

row 6

row 7

row 8

row 9

row 10

row 11

row 12

Details

A mask, not an overlay

The usual way to fade a scroll area is a gradient overlay painted in the background colour, which only works while the background stays flat and opaque: put the same trick over an image, a glow or a themed surface and the seam shows.

Here the fade is a mask-image applied to the element itself, so the content becomes genuinely transparent rather than being covered. Whatever is behind shows through, in both themes, with no colour to keep in sync.

css
mask-image: linear-gradient(
  to bottom,
  transparent var(--offset-top),
  #000 calc(var(--offset-top) + var(--fade-top)),
  #000 calc(100% - var(--offset-bottom) - var(--fade-bottom)),
  transparent calc(100% - var(--offset-bottom))
);

The consequence worth knowing: the mask applies to the whole element, scrollbars included, and it cannot be undone locally. Anything that must stay fully opaque belongs outside the masked element.

Fade and offset

fade is the length over which the content dissolves; offset is where that dissolve begins. Keeping them separate is what allows a fade to start below a fixed header instead of at the top of the element.

tsx
<MaskGradientY offsetTop="var(--spacing-navbar)" fadeTop="0px" fadeBottom="var(--spacing-page-offset)">
  <nav className="overflow-y-scroll"></nav>
</MaskGradientY>

This is the sidebar of this site: everything above the navbar height is fully transparent — offsetTop with a zero fadeTop gives a hard cut rather than a gradient — while the bottom dissolves over the page offset. Both take any CSS length, custom properties included, so the values can be tokens rather than numbers.

Only the axis it names

The component fades vertically and nothing else: there is no horizontal counterpart, because a horizontal fade almost always means the layout is hiding an overflow it should have solved. If you need one, the same mask written on the other axis is three lines of CSS.