170

Lens Focus Pull Scroll

£7
or the whole Vault, £99

Pinned scroll section that works a camera lens: focus pulls from the city to the photographer with real depth-of-field blur, a focus ring and aperture ring turn past an index mark, the lens stops down until both planes sharpen, and the shutter fires to leave a printed photo.

ScrollTrigger Lenis advanced
5 more details
pinningscrubvelocity-reactivesvg-animationscroll-storytelling

Released

About this effect

Scroll operates a camera lens. The frame is what the lens sees: a photograph split into two depth planes, the photographer in front and Lisbon at dusk behind, each blurred on its own. It opens wide open at f/1.4 focused on the city, with the subject soft in front of it. Scrolling turns the focus ring, the distance scale sliding past the index mark from infinity to 2 m, and the city melts into bokeh as the subject comes sharp, with a couple of percent of focus breathing as it goes.

Read the full effect overview

Then the aperture ring turns to f/8 and depth of field deepens until both planes are nearly sharp. The frame dips darker as the aperture closes and recovers as the shutter speed readout compensates. At the end the viewfinder blacks out for an instant as the shutter fires, and the taken frame settles onto the page as a bordered print. A quick flick of the wheel overshoots the focus, and once the scroll stops it hunts back and forth and settles like autofocus.

Lens Focus Pull Scroll - GSAP animation effect preview

What's included

8 items
  • Depth of field from a thin-lens model: each plane's blur follows its distance, the focus distance and the f-number, so the pull reads as optical rather than a linear fade
  • Two aligned planes from one photograph (a cut-out subject over a background plate), blurred independently with CSS filter blur and nothing else
  • Focus ring with a 0.5 m to infinity distance scale and a depth-of-field bracket that widens as you stop down
  • Aperture ring with a full f-stop scale turning past the same index mark
  • Exposure that dips as the aperture closes and recovers as the shutter speed readout compensates
  • A brief viewfinder blackout as the shutter fires, then the frame lands as a bordered print with the scales left at their final values
  • Scroll velocity overshoots the focus and it hunts back, autofocus style, once the wheel stops
  • Phone layout with a portrait crop and the ring control along the bottom; reduced motion shows the finished photo with no pin

Perfect for

5 use cases
  • Camera, lens and phone-camera launch pages explaining focus, aperture and depth of field as the visitor scrolls
  • Photographer and videographer portfolios that open on a signature image coming into focus
  • Photography courses and workshops illustrating what aperture actually does to a picture
  • Travel and hospitality stories where a scene resolves from the view to the person in it
  • Agency case studies and editorial features that need a cinematic reveal for a single hero photograph

How it works

2 sections

ScrollTrigger pins the section and scrubs it through four beats: focus pull, stop down, shutter and print. At every point the lens has a real focus distance and f-number, and each photo plane is blurred by the amount a lens would give it at that setting, so the near and far planes soften and sharpen independently and the change in sharpness reads like a real lens.

The focus and aperture rings turn around the lens axis so their markings slide past the index, with a depth-of-field bracket on the distance scale that widens as the lens stops down. Scroll speed briefly pushes the focus past its mark and it hunts back after the wheel stops, and passing the shutter beat fires a short timed blackout before the frame settles into a print.

Plugins ScrollTrigger
Difficulty Advanced
Smooth scroll Lenis integration
Includes HTML + JS + CSS source, documentation, AI setup prompt, lifetime updates

Lighthouse, as measured

Google Lighthouse on this effect's demo, from the latest scan. A measurement of the demo as shipped, not a promise for your page.

Accessibility
100
Best practices
100

No performance score, on purpose. That figure depends on how you deploy: your server's compression and caching, your CDN, the connection and the device doing the test, none of which the code controls. The same page can score very differently on two consecutive runs, so measure it where it will live.

The Vault opens this source and the whole library.

Each product gives you the HTML, CSS and JavaScript source, the full documentation and an AI setup prompt, licensed for unlimited personal and commercial projects. Every paid product is tested for desktop and mobile layouts, reduced-motion handling, animation cleanup and keyboard controls wherever there is something to operate.

One payment, over 300 products, and future additions to the library.

Documentation

Quick Start

1. Add to your HTML <head>:

In your downloadThe working code for this step, in index.html.

2. Add before the closing </body> tag:

In your downloadThe working code for this step, in index.html.

3. Add the section where it should play:

In your downloadThe working code for this step, in index.html.

Both ring scales are generated by the script, so the dial SVG starts empty.

Using It With Your Own Design

What the effect needs from your markup: a [data-lens] section containing [data-lens-frame], [data-lens-scene] and the two images [data-lens-far] (the background plate) and [data-lens-near] (the subject cut-out, a transparent WebP or PNG). Everything else is optional: leave out [data-lens-dial] for no rings, the readout spans, the exposure, blink and flash layers, or the cue, and the rest keeps working. The section is pinned, so it should be a direct, full-height block in the page flow.

What is only the demo's styling: the Inter type, the dark ground, the print border colour (--paper), the accent on the index mark (--accent), the bottom scrim, the cue pill and the readout layout. Restyle or remove any of them.

CSS the effect depends on:

  • .lens__frame needs overflow: hidden and positioning; its box-shadow reads the --print custom property (0 to 1) that the script sets for the print beat.
  • .lens__scene is oversized (inset: -3%) so blurred edges never pull the ground in at the frame border. Keep both planes in it with the same object-fit: cover and the same object-position, or they will not line up.
  • The .has-js start state blurs the near plane before the first render, so the first painted frame is already the wide-open, focused-on-the-background state. It is lifted under reduced motion.
  • .lens__control sits over the frame on desktop and below it on phones (the max-width: 640px block); the script reads the frame and dial sizes on every refresh, so you can move either.

Choosing Your Images

The depth effect is only as convincing as the split between the two planes.

  • Start from one deep-focus photograph where both the subject and the background are sharp (a smaller aperture or a wider lens). The effect adds the blur; it cannot remove blur baked into the photo.
  • Cut the subject out cleanly with a good mask and check the edges at full size, especially hair against sky. Export it at the same pixel size as the background so the two stay registered.
  • Fill the hole in the background plate behind the subject. It only shows through the soft edge of the subject and when the background is blurred, so a smooth fill is enough; leaving the subject in the plate gives it a ghost halo when the background blurs.
  • Set the phone crop with object-position in the max-width: 640px block.

Options

Set these as data attributes on [data-lens].

Attribute Values Default Description
data-lens-subject metres 2 Distance of the near plane; the focus pull ends here
data-lens-background metres or inf inf Distance of the far plane
data-lens-start-focus metres or inf inf Where focus starts, normally the background
data-lens-open 1 to 4 1.4 Wide-open f-number at the start
data-lens-stop 2 to 22 8 f-number the aperture stops down to
data-lens-focal 24 to 200 (mm) 50 Focal length used for the depth-of-field maths
data-lens-strength 0 to 3 1 Multiplier on every blur radius
data-lens-velocity 0 to 3 1 Strength of the focus overshoot from scroll speed; 0 turns it off
data-lens-print true, false true End on the shutter and print beat; false holds the stopped-down view
data-lens-runway px 2400 Scroll distance of the pinned sequence on desktop
data-lens-mobile-runway px 1900 Scroll distance when the frame is narrower than 640px
data-lens-static 0 to 1 none Render one frozen progress with no pin, e.g. for a still or a print layout

Accessibility

  • Reduced motion: nothing is pinned and nothing scrolls. The section renders the finished photo, sharp, with the rings and readout at their end values (2 m, f/8, 1/125 s). Switching the preference while the page is open rebuilds the section in the right mode.
  • Without JavaScript: both planes show unblurred, so the visitor sees the original photograph with the readout. The cue stays hidden.
  • Screen readers: the rings and decorative layers are aria-hidden. The subject image carries the photograph's description and the section's aria-label describes the sequence; the readout is plain text.
  • Touch: ordinary page scrolling drives everything; the focus overshoot is gentler on touch screens.

Dependencies

Required:

  • GSAP 3.12+ (the demo pins 3.15.0)
  • ScrollTrigger

Optional:

  • Lenis 1.3.17 for smooth scrolling. Add ?smooth=off to the URL, or data-smooth="off" on <html>, to compare without it.

Performance

Blur is applied with CSS filters on exactly two layers, and only when a plane is meaningfully out of focus; sharp planes have no filter at all. There is no animation loop of its own: the scene renders only when the scroll position, the focus hunt or the shutter changes it. Keep both images near 2400px wide (the demo's are about 175 KB and 100 KB) so the filter has a sensible amount to work on.

Demo Photography

The photograph is a man taking a picture of Lisbon on Pexels, used under the Pexels License. The bundled files are derivatives made for this effect: subject.webp is the man cut out of the frame with a subject mask, and plate.webp is the same frame with him removed and the gap filled. Both are cropped so the railing in the foreground is out of shot. Replace them with your own pair of registered exports.

Also in your download: the full source, worked examples, the events and programmatic API, and the class reference.

£7