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.

Sharpens into focus on enter.
No fade, just blur.

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.

intensity = subtlefrom 8px
intensity = mediumfrom 16px
intensity = strongfrom 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.

fade
true
fade
false
<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.

direction="in" — blurred → sharp as I enter.
direction="out" — sharp → blurred as I scroll 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.

Scrub me — blur tracks scroll
<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.

One
Two
Three
Four
{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.

BOOGIE.RO · 0042
Motion that reads as craft
<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

Props

PropTypeDefaultDescription
intensity'subtle' | 'medium' | 'strong'Preset blur amount — 8 / 16 / 28 px. Shorthand for `from`. Ignored if `from` is set explicitly.
fromnumber16Starting blur in px (or ending blur with `direction="out"`). Overrides `intensity`.
tonumber0Final blur in px on the resolved state.
direction'in' | 'out''in'`in` sharpens on enter (default). `out` blurs on leave — pairs naturally with `scrub`.
fadebooleantruePair the blur with an opacity fade (0 → 1 on enter, 1 → 0 on leave).
durationnumber1.0Tween duration in seconds.
easestring'power3.out'GSAP ease.
startstring'top 85%'ScrollTrigger `start`.
endstring'bottom 60%'ScrollTrigger `end` — only meaningful when `scrub` is set.
scrubboolean | numberfalseTie the blur to scroll progress. `true` for tight tracking, a number for inertia (seconds of lag).
oncebooleantruePlay the reveal only once. Set `false` to reverse when scrolling back out.
delaynumberDelay before the tween starts (seconds).
asElementType'div'Element to render — `"section"`, `"figure"`, etc.