Fade
A soft mask that dissolves a box into its surface at its edges. Use the static fade-* utilities for a constant bleed, or the scroll-fade-* siblings to track scroll position. Pure CSS, no JS.
Static fade#
The base primitive: a constant mask-image gradient that fades one or more edges into the surface, no matter the scroll position. Reach for it whenever the bleed is fixed - a marquee that drifts in from both sides, a masonry that clips at the section edges, or a preview that peeks up from the bottom of its frame.
// A marquee whose tiles dissolve into the band at both edges, at any scroll position.
<div className="group/marquee overflow-hidden fade-x [--fade-size:10%]">
{/* Render the set twice so the -50% marquee loops with no seam. */}
<div className="flex w-max animate-marquee gap-3 pr-3 group-hover/marquee:[animation-play-state:paused] motion-reduce:[animation:none]">
{[...logos, ...logos].map((logo, i) => (
<Logo key={i} {...logo} />
))}
</div>
</div>fade
fade-x
fade-t
fade-b
fade-l
fade-r
Depth per edge is the --fade-size knob (default min(2.5rem, 12%)); set it with an arbitrary property like [--fade-size:6%]. Inside that band the alpha runs on an eased smoothstep, not a straight line: a linear ramp turns a corner at each end of the band and the eye reads those corners as edges, which is exactly what the fade is there to avoid. It only gets more visible the deeper the band, so a long dissolve like LoadMore’s depends on it. The rest of this page covers scroll-fade, the same mask made to track scroll progress instead.
Scroll-aware preview#
Scroll the list. At the top the leading edge is crisp; once content slides under it, the top fades while the bottom keeps hinting at more below. Change the fade depth to feel the --scroll-fade-size knob.
- Mara deployed api-gateway to production2 minutes ago
- Build #2847 passed on main11 minutes ago
- Theo commented on “Density tokens”26 minutes ago
- New sign-up: northwind.coAbout an hour ago
- Weekly usage report is ready3 hours ago
- Priya invited 4 teammatesYesterday, 18:40
- Billing: invoice #0231 settledYesterday, 09:12
- Backup completed for eu-west-12 days ago
- Domain koalaui.com renewed3 days ago
- Lin closed 6 issues in “Polish”4 days ago
- API key rotated by automation5 days ago
- Region us-east-2 came back onlineLast week
How it works#
The fade is a mask-image linear gradient whose two inner stops are driven by animation-timeline: scroll(self) - a scroll-progress timeline, not a clock. Each edge’s depth is a registered@property number from 0 (crisp) to 1 (faded), so it interpolates as you scroll: the top fades in over the first --scroll-fade-reveal of travel, and the bottom sharpens over the last. No scroll listeners, no JavaScript, no layout cost.
Put the mask on the scroll container itself, and the border on a wrapper - the gradient masks whatever it covers, so a border on the same element would fade too. The frame stays crisp; only the content dissolves into the edge.
Usage#
Wrap the scroll area in a bordered, overflow-hidden frame, then put scroll-fade on the inner scrolling element.
// Frame owns the border + corners; the inner element owns the scroll + mask.
<div className="overflow-hidden rounded-xl border border-border">
<ul className="scroll-fade h-72 overflow-y-auto p-2">
{items.map((item) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
</div>scroll-fade
scroll-fade-x
scroll-fade-t
scroll-fade-b
scroll-fade-l
scroll-fade-r
Horizontal#
scroll-fade-x masks the inline edges instead. The right edge fades while there are more chips to reach, and sharpens at the end of the row.
<div className="overflow-hidden rounded-xl border border-border p-2">
<div className="scroll-fade-x flex gap-2 overflow-x-auto px-2 py-1">
{tags.map((tag) => (
<Chip key={tag}>{tag}</Chip>
))}
</div>
</div>Single edge#
Reach for a one-sided utility when only one edge should bleed - a chat log that fades into its header (scroll-fade-t), or a list that only needs to hint at more rows below (scroll-fade-b). The crisp edge never fades, even mid-scroll.
- Aurora
- Bennett
- Cassidy
- Delphine
- Emerson
- Florian
- Greer
- Halcyon
- Indra
- Jovan
- Kestrel
- Lowell
- Aurora
- Bennett
- Cassidy
- Delphine
- Emerson
- Florian
- Greer
- Halcyon
- Indra
- Jovan
- Kestrel
- Lowell
Customizing#
Two CSS variables tune the effect per element - set them with an arbitrary property so they cascade cleanly and stay overridable.
--scroll-fade-size
min(2.5rem, 12%)
--scroll-fade-reveal
6rem
// Deeper fade, quicker reveal.
<ul className="scroll-fade overflow-y-auto [--scroll-fade-size:4rem] [--scroll-fade-reveal:3rem]">
{/* … */}
</ul>Browser support#
The scroll-aware behavior rides scroll-driven animations. Where they aren’t supported, a @supports fallback keeps a static fade on the utility’s edges, so the hint never disappears - it just stops tracking the scroll position. Because the effect is driven by the user’s own scrolling (it never plays on its own), it’s left on under prefers-reduced-motion.