Blur
Scroll-driven blur reveal — wrap any node and it'll resolve into focus as it enters the viewport. Three intensity presets, optional opacity fade, in / out directions, scrub mode for parallax-style tracking, and automatic respect for prefers-reduced-motion. GSAP under the hood via @bwo-ui/core's createBlur factory.
Usage
import { Blur } from '@bwo-ui/react';
{/* Preset intensity — most common */}
<Blur intensity="strong">
<img src="/hero.jpg" alt="" />
</Blur>
{/* Explicit pixel control */}
<Blur from={24} to={0} duration={1.2}>
<h1>Headline</h1>
</Blur>
{/* No fade, just blur */}
<Blur intensity="medium" fade={false}>
<p>Quiet reveal — sharpens without changing opacity.</p>
</Blur>Intensity
Three presets cover the common cases — subtle (8 px), medium (16 px, the default), strong (28 px). Use subtle for ambient reveals on long content (paragraph headings inside a scrolling article), medium as the safe general-purpose choice, strong for hero positions where the focus-into-place effect should feel cinematic.
from 8pxfrom 16pxfrom 28px<Blur intensity="subtle">…</Blur>
<Blur intensity="medium">…</Blur> {/* default */}
<Blur intensity="strong">…</Blur>For pixel-perfect control, set from directly — from takes precedence over intensity.
<Blur from={20} duration={1.2}>…</Blur>Fade
By default the blur is paired with an opacity fade (0 → 1 on enter). Set fade={false} for a quieter reveal where the element stays visible but goes from blurred to sharp — useful when the element underneath already has a strong colour treatment you don't want to mute.
<Blur intensity="medium" fade>…</Blur> {/* default */}
<Blur intensity="medium" fade={false}>…</Blur> {/* blur only */}Direction
direction="in" (default) animates from blurred to sharp as the element enters the viewport. direction="out" inverts the timing — the element starts sharp, then blurs as it leaves the viewport. Combine with scrub for a parallax section that softens as the user scrolls past.
{/* Sharpen on enter */}
<Blur direction="in" intensity="strong">…</Blur>
{/* Blur on exit — pairs naturally with scrub */}
<Blur direction="out" intensity="strong" scrub end="top 30%">…</Blur>Scrub
With scrub, the blur amount is tied directly to scroll progress between start and end — scroll up and the blur grows back. Use it for cinematic hero sections that resolve as the user reaches a comfortable reading position, or paired with direction="out" for a section that softens as you scroll past it.
<Blur intensity="strong" scrub start="top 90%" end="top 40%">
<HeroSection />
</Blur>Stagger
Sequence a row of blurs with incremental delay values to get a graceful cascade. There's no built-in stagger primitive — just multiply the delay by the index. This pattern is the reason most marketing pages feel alive.
{items.map((item, i) => (
<Blur key={item.id} intensity="medium" delay={i * 0.12}>
<Card>{item.label}</Card>
</Blur>
))}Recipes
Photo reveal
Hero photos benefit from a stronger intensity (24 – 32 px) and a slightly longer duration (~1.2 s). The blur-into-focus effect mimics a camera pulling sharp, which reads as deliberate, considered design.
<Blur intensity="strong" duration={1.2}>
<div style={{ position: 'relative', borderRadius: 14, overflow: 'hidden' }}>
<img src="/hero.jpg" alt="" style={{ width: '100%', display: 'block' }} />
<div style={{ position: 'absolute', inset: 0, padding: 22, color: '#fff' }}>
<h2>Motion that reads as craft</h2>
</div>
</div>
</Blur>Staggered card grid
For three-up feature grids on a landing page, stagger the blurs at ~ 150 ms intervals. The pattern feels deliberate without overstaying its welcome.
Performance
GSAP-tuned animations under 16 ms.
Accessibility
Reduced-motion respected at the factory layer.
Composable
Drop a <Blur> around any node. No magic.
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 12 }}>
{features.map((item, i) => (
<Blur key={item.title} intensity="medium" delay={i * 0.15}>
<Card>
<h3>{item.title}</h3>
<p>{item.body}</p>
</Card>
</Blur>
))}
</div>Reduced motion
The core factory checks window.matchMedia('(prefers-reduced-motion: reduce)').matches on mount. If the user has reduced motion enabled, the element snaps directly to its final state — no blur, no opacity fade, no scroll trigger. No extra code required on your side.
Accessibility
- Blur is a visual effect; it doesn't change the DOM's reading order or semantics. Whatever you wrap remains fully readable by screen readers.
- Heavy blur during the initial frames can hurt readability for low-vision users. Stick to
subtle/mediumfor text-dense content; reservestrongfor hero imagery. prefers-reduced-motion: reducebypasses the animation entirely — the element appears in its final state without any transition.- Avoid wrapping interactive elements (buttons, links) in long-running scrub blurs — users may try to click them while still blurred, which feels broken.
- The
filter: blur()CSS property creates a new stacking context. If the blurred element containsposition: fixedchildren, they will be positioned relative to the blur wrapper while the animation runs.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
intensity | 'subtle' | 'medium' | 'strong' | — | Preset blur amount — 8 / 16 / 28 px. Shorthand for `from`. Ignored if `from` is set explicitly. |
from | number | 16 | Starting blur in px (or ending blur with `direction="out"`). Overrides `intensity`. |
to | number | 0 | Final blur in px on the resolved state. |
direction | 'in' | 'out' | 'in' | `in` sharpens on enter (default). `out` blurs on leave — pairs naturally with `scrub`. |
fade | boolean | true | Pair the blur with an opacity fade (0 → 1 on enter, 1 → 0 on leave). |
duration | number | 1.0 | Tween duration in seconds. |
ease | string | 'power3.out' | GSAP ease. |
start | string | 'top 85%' | ScrollTrigger `start`. |
end | string | 'bottom 60%' | ScrollTrigger `end` — only meaningful when `scrub` is set. |
scrub | boolean | number | false | Tie the blur to scroll progress. `true` for tight tracking, a number for inertia (seconds of lag). |
once | boolean | true | Play the reveal only once. Set `false` to reverse when scrolling back out. |
delay | number | — | Delay before the tween starts (seconds). |
as | ElementType | 'div' | Element to render — `"section"`, `"figure"`, etc. |