Progressive Blur
A graduated blur — stacked backdrop-filter layers, each masked to an overlapping gradient band, so the blur ramps smoothly instead of stepping. Shown fading the edges of a marquee.
Usage
import { ProgressiveBlur } from "~/components/effects/progressive-blur";// Fade the left edge of scrolling content into blur<div className="relative overflow-hidden"><Marquee>{logos}</Marquee><ProgressiveBlurdirection="to-left"blurEnd={8}layers={5}className="absolute inset-y-0 left-0 z-10 w-40"/></div>
Why stack layers
A single backdrop-filter: blur() is uniform: everything behind it gets the same blur, with a hard edge where the element ends. To make blur ramp, you stack several blur layers and mask each one to a different band.
The blur amounts grow geometrically (1, 1.7, 2.8, 4.8, 8…), and each layer is masked with a linear-gradient that fades in, holds, and fades out over a slice of the width. Because neighbouring bands overlap, the blurs cross-fade into each other and you read one continuous gradient of blur rather than five discrete steps. Point the gradient with direction and drop one over each edge of a marquee for the effect above.
// Each layer's blur grows geometrically; each is masked to a band that// overlaps its neighbours, so the blur ramps instead of stepping.const step = 1 / (layers + 1);const blurBase = blurRange ** (1 / (layers - 1));layers.map((layer) => (<div style={{backdropFilter: `blur(${blurBase ** layer}px)`,maskImage: `linear-gradient(to left,transparent ${layer * step * 100}%,black ${(layer + 1) * step * 100}%,black ${(layer + 2) * step * 100}%,transparent ${(layer + 3) * step * 100}%)`,}} />));
Props
| prop | type | default | description |
|---|---|---|---|
| direction | "to-left" | "to-right" | "to-top" | "to-bottom" | "to-bottom" | Which way the blur deepens. |
| blurStart | number | 0 | Blur (px) at the shallow edge. |
| blurEnd | number | 12 | Blur (px) at the deep edge. |
| layers | number | 5 | Layer count. More = smoother ramp. |
| …props | HTMLAttributes | — | className/style for size + position. |
Recreated from frostin-ui's ProgressiveBlur.
Source
download .zipThe full implementation. Copy it in or grab the zip. It leans on a cn() class helper and the theme tokens (--primary, --border, …).
import { cn } from "~/lib/utils";export type BlurDirection = "to-left" | "to-right" | "to-top" | "to-bottom";const DIR_CSS: Record<BlurDirection, string> = {"to-left": "to left","to-right": "to right","to-top": "to top","to-bottom": "to bottom",};function linearMask(direction: BlurDirection,opacities: number[],positions: number[]) {const stops = positions.map((p, i) => `rgba(0,0,0,${opacities[i]}) ${(p * 100).toFixed(3)}%`).join(", ");return `linear-gradient(${DIR_CSS[direction]}, ${stops})`;}export interface ProgressiveBlurPropsextends React.HTMLAttributes<HTMLDivElement> {direction?: BlurDirection;/** Blur (px) at the shallow edge. Default 0. */blurStart?: number;/** Blur (px) at the deep edge. Default 12. */blurEnd?: number;/** Number of blur layers. More = smoother ramp. Default 5. */layers?: number;}/*** A progressive (graduated) blur: a stack of layers whose `backdrop-filter`* blur grows geometrically, each masked to an overlapping gradient band, so the* blur ramps smoothly across the element instead of stepping. Place it over the* edge of scrolling content for a clean fade-to-blur. Recreated, dependency-free,* from frostin-ui's ProgressiveBlur.*/export function ProgressiveBlur({direction = "to-bottom",blurStart = 0,blurEnd = 12,layers = 5,className,style,children,...rest}: ProgressiveBlurProps) {const n = Math.max(layers, 2);const step = 1 / (n + 1);const blurMin = Math.min(blurStart, blurEnd);const blurMax = Math.max(blurStart, blurEnd);const blurRange = blurMax - blurMin;const blurBase = blurRange ** (1 / (n - 1));return (<div className={cn("relative", className)} style={style} {...rest}>{children}<divclassName="pointer-events-none absolute inset-0"style={{backdropFilter: `blur(${blurMin}px)`,WebkitBackdropFilter: `blur(${blurMin}px)`,}}/>{Array.from({ length: n }, (_, layer) => {const bf = `blur(${(blurMin + blurBase ** layer).toFixed(3)}px)`;const positions = [layer * step,(layer + 1) * step,(layer + 2) * step,(layer + 3) * step,];const img = linearMask(direction, [0, 1, 1, 0], positions);return (<divkey={layer}className="pointer-events-none absolute inset-0 rounded-[inherit]"style={{backdropFilter: bf,WebkitBackdropFilter: bf,maskImage: img,WebkitMaskImage: img,}}/>);})}</div>);}