A blur that ramps up toward an edge instead of switching on at a line, made of stacked backdrop-filter layers, for scrolling lists, headers, footers and image captions.
- Quarterly report
- Design review notes
- Onboarding checklist
- Pricing experiment
- Launch plan
- Customer interviews
- Roadmap draft
- Retro summary
- Hiring pipeline
- Security audit
import { ProgressiveBlur } from "@/components/ballmac/progressive-blur"
const rows = ["Quarterly report", "Design review notes", "Onboarding checklist", "Pricing experiment", "Launch plan", "Customer interviews", "Roadmap draft", "Retro summary", "Hiring pipeline", "Security audit"]
export default function ProgressiveBlurDemo() {
return (
<div className="relative h-72 w-full max-w-sm overflow-hidden rounded-2xl border bg-card">
<div className="h-full overflow-y-auto p-3" tabIndex={0} role="region" aria-label="Documents">
<ul className="grid gap-2 py-8">
{rows.map((r, i) => (
<li key={r} className="flex items-center gap-3 rounded-xl border bg-background p-3">
<span aria-hidden="true" className="flex size-8 items-center justify-center rounded-lg bg-muted font-mono text-xs text-muted-foreground">{i + 1}</span>
<span className="text-sm font-medium">{r}</span>
</li>
))}
</ul>
</div>
<ProgressiveBlur position="top" size="4rem" strength={12} />
<ProgressiveBlur position="bottom" size="5rem" strength={16} />
</div>
)
}Installation
$ pnpm dlx shadcn@latest add @ballmac/progressive-blurCopy the source into your project.
components/ballmac/progressive-blur.tsx// Ballmac UI: Progressive Blur. https://ui.ballmac.com/components/progressive-blur import * as React from "react" import { cn } from "@/lib/utils" type ProgressiveBlurProps = Omit<React.ComponentProps<"div">, "children"> & { /** Edge the blur is attached to. It is strongest right at the edge. */ position?: "top" | "bottom" | "left" | "right" /** Depth of the blurred band: a CSS length such as "6rem" or "30%". */ size?: string /** Strongest blur in pixels, reached at the edge. */ strength?: number /** Number of stacked layers. More layers make a smoother ramp and cost a little more to paint. */ layers?: number } // The gradient runs from the inner edge of the band toward the attached edge, where the blur is strongest. const TO = { top: "to top", bottom: "to bottom", left: "to left", right: "to right" } as const /** * A blur that ramps up toward an edge instead of switching on at a line, made from stacked * backdrop-filter layers with staggered masks. Pointer events pass through it. Decorative. */ function ProgressiveBlur({ position = "bottom", size = "5rem", strength = 14, layers = 8, className, style, ...props }: ProgressiveBlurProps) { const horizontal = position === "left" || position === "right" const n = Math.max(2, Math.min(layers, 12)) return ( <div data-slot="progressive-blur" aria-hidden="true" className={cn("pointer-events-none absolute z-10", horizontal ? "inset-y-0" : "inset-x-0", className)} style={{ [position]: 0, [horizontal ? "width" : "height"]: size, ...style }} {...props} > {Array.from({ length: n }, (_, i) => { // Each layer fades in over a window of the band and is blurred a little more than the one before. const from = (i * 100) / (n + 1) const blur = (strength * (i + 1)) / n const mask = `linear-gradient(${TO[position]}, transparent ${from}%, black ${from + 100 / (n + 1)}%, black ${from + 200 / (n + 1)}%, transparent ${from + 300 / (n + 1)}%)` return ( <span key={i} className="absolute inset-0" style={{ backdropFilter: `blur(${blur.toFixed(2)}px)`, WebkitBackdropFilter: `blur(${blur.toFixed(2)}px)`, maskImage: mask, WebkitMaskImage: mask, }} /> ) })} </div> ) } export { ProgressiveBlur, type ProgressiveBlurProps }Update the import paths to match your project setup.
Usage
import { ProgressiveBlur } from "@/components/ballmac/progressive-blur"The full example is in the Code tab above.
Examples
Four edges
import { ProgressiveBlur } from "@/components/ballmac/progressive-blur"
function scene() {
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="400" height="260"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#f97316"/><stop offset=".5" stop-color="#db2777"/><stop offset="1" stop-color="#6366f1"/></linearGradient></defs><rect width="400" height="260" fill="url(#g)"/><g fill="none" stroke="rgba(255,255,255,.7)" stroke-width="3">${Array.from({ length: 9 }, (_, i) => `<circle cx="${60 + i * 36}" cy="${130 + Math.sin(i) * 50}" r="${14 + (i % 3) * 8}"/>`).join("")}</g></svg>`
return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`
}
const sides = ["top", "right", "bottom", "left"] as const
export default function ProgressiveBlurSides() {
return (
<div className="grid w-full max-w-xl grid-cols-2 gap-3">
{sides.map((position) => (
<div key={position} className="relative aspect-[3/2] overflow-hidden rounded-xl border">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={scene()} alt="" className="size-full object-cover" />
<ProgressiveBlur position={position} size="45%" strength={14} />
<span className="absolute top-2 start-2 z-20 rounded bg-black/55 px-1.5 py-0.5 font-mono text-[11px] text-white">{position}</span>
</div>
))}
</div>
)
}API reference
| Prop | Type | Default |
|---|---|---|
positionEdge the blur is attached to. It is strongest right at the edge. | "top" | "bottom" | "left" | "right" | "bottom" |
sizeDepth of the blurred band: a CSS length such as "6rem" or "30%". | string | "5rem" |
strengthStrongest blur in pixels, reached at the edge. | number | 14 |
layersNumber of stacked layers. More layers make a smoother ramp and cost a little more to paint. | number | 8 |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Screen readers | Decorative: aria-hidden and pointer-events-none |
| Interaction | Never blocks clicks or scrolling |
Use with AI
<ProgressiveBlur position='top|bottom|left|right' size strength layers /> inside a relative parent that scrolls or holds an image. Pointer events pass through it. A server component with no JavaScript. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Progressive Blur (@ballmac/progressive-blur) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Scrolling lists that fade out at the edges
- Text over images, instead of a dark gradient
Not for
- Browsers or surfaces without backdrop-filter support (it degrades to no blur)
Registry JSON: https://ui.ballmac.com/r/progressive-blur.json
Credits
Free to use in personal and commercial projects.
- npm
- None
- Registry
- shadcn/utils
Pairs well with
Scroll Area
A focusable, named scroll region with theme-aware custom thumb and native scrolling behavior for long lists and documents.
Card
A composable content surface with compact spacing, an action slot, and optional interactive feedback.
Animated Beam
An SVG beam that connects two elements with a curved path and sends a glowing gradient pulse along it. Follows layout changes, pauses off-screen, static under reduced motion.
Animated Grid
A decorative hairline grid background where a few random cells softly light up and fade, like instrument lights. SVG, hydration-safe, static under reduced motion.