Play Pause

ProScrollpowered by @vectorvesper/motion

A column of posters travelling slowly upward between the words PLAY and PAUSE. Each one is rolled into a tube; as it reaches the middle it unrolls itself, holds long enough to be read, and rolls back. Hover any of them and the column stops for you. The whole ticker is one scene, so the poster count costs geometry rather than browser resources.

Play Pause

Included with membership

Members install Play Pause with one command and get the full source, the usage code and the Framer remix, licensed for client work. One membership covers the whole library and the CLI.

When to use it

A showreel that should feel like it is running whether or not anyone is watching — a studio index, a case-study list, a film or fashion landing page. It takes a full screen and reads vertically, so it wants a section of its own rather than a slot in a grid.

  • Define posters outside the component or memoise it. A new array identity rebuilds the scene and re-uploads every texture.
  • Vary rollRadius and twists a little between items. Identical rolls read as a repeat; small differences make it look like a stack of real posters.
  • Keep the images roughly portrait. Each poster is 300 tall and as wide as its own aspect makes it — nothing is cropped, so a landscape image simply comes out wide.
  • Give it real height. The column is the height of the component, and a poster unrolls when it reaches the middle of it.
  • Keep the set between four and twelve. Fewer and the loop is obvious; more and you are paying for geometry nobody scrolls to.

Avoid:

  • Do not expect the posters to unroll on their own under reduced motion. The travel is what brings a poster to the middle, and that is what stops — hover still works.
  • Do not use a very wide source. Poster width follows the image's aspect and the scene is about 420 wide, so a panorama runs past the edge and is clipped there.
  • Do not put another full-screen WebGL component directly above or below it. Each one holds a drawing context, and browsers only allow a handful at once.
  • Do not expect the two words to stay put. They part to clear each poster as it opens, so their resting position is a starting point rather than a layout you can measure against.

Props

A column of posters travelling slowly upward, with the words PLAY and PAUSE set enormous behind them. Each poster is rolled into a tube; as it reaches the middle of the column it unrolls itself, holds long enough to be read, and rolls back up before moving on. The two words part just far enough to clear whichever poster is open, so a wide image pushes them further than a narrow one. Hover any of them and the column stops for you. Colour arrives with the unroll — rolled posters are greyscale.

<PlayPause />

The full arrangement — the travelling column, the typography behind it, and the hover and auto-unroll behaviour. Paints its own background and fills its container, with a 440px floor.

PropTypeDefaultDescription
posters*PlayPauseItem[]—The posters, in order. The list is doubled internally so the loop has no seam — you supply each one once.
playTextstring"PLAY"The word on the left. It slides outward and takes the accent colour while a poster is open.
pauseTextstring"PAUSE"The word on the right.
accentColorstring"#F5FD2A"Colour the two words take while a poster is unrolled. They return to white when it closes.
tickerDurationnumber25Seconds for the column to travel one full set of posters. Longer is slower; the loop is seamless either way.
textGapnumber32Pixels of clearance left between the words and the poster that is opening. The two words part by however much that poster needs, plus this — so a wide image pushes them further than a narrow one.
classNamestring""Applied to the wrapper.
mount"auto" | "eager""eager"When to build the scene. The default builds it on mount. Set "auto" to wait until the section is near the viewport and the page has frame headroom. The whole strip is drawn in one scene rather than one per poster, so its cost does not grow with the number of items — but it is still a 3D scene, and deferring it below the fold is free. Neither setting takes it off the shared frame loop or overrides the reduced-motion preference. A scene running its own loop would be invisible to the scheduler, so it could not be set aside under load, and it would slow every other animation on the page rather than only itself.
disableAdaptiveQualitybooleanfalseKeeps the pixel ratio at whatever the device reports, up to 2. By default the runtime caps it on lower tiers. This is not the same as dpr, which pins a fixed value: this one still follows the device, it just stops the runtime throttling it.

<PlayPauseItem />

type

One poster. The three roll values are what stop the column looking mechanical.

PropTypeDefaultDescription
id*string—Stable key for the poster.
src*string—Image URL. Every poster is 300 tall and takes whatever width its own aspect gives it, so nothing is cropped — a landscape source simply comes out wide.
rollRadius*number—How thick the tube is when rolled. Around 0.5 to 0.65 reads as poster stock.
twists*number—How many turns the roll makes. About 1 is a single loose curl; higher winds it tighter.
radiusFalloff*number—How much the roll narrows toward its centre. 0 is an even tube; 0.15 tapers it noticeably.

<PlayPausePoster />

A single poster on its own, without the column or the typography. It unrolls on hover and fills whatever box you give it. Useful as a standalone card; it carries its own canvas, so use a handful at most on one page.

PropTypeDefaultDescription
src*string—Image URL.
fit"cover" | "contain" | "fill""fill"How the image sits in its box.
planeWidthnumber3.5Width in scene units, used when fit is "fill".
rollRadiusnumber0.5Thickness of the roll.
twistsnumber2.0Turns in the roll.
radiusFalloffnumber0.8How much the roll narrows toward its centre.
animationSpeednumber3.0How quickly it unrolls toward the target.
hoverProgressnumber1.0How far it unrolls when hovered. 1 is fully flat.
restProgressnumber0.0How far it stays unrolled at rest. 0 is fully rolled.
initialProgressnumber0.0Unroll amount when hover interaction is off.
enableHoverInteractionbooleantrueWhether hovering unrolls it. Off, it holds initialProgress.
enableGrayscalebooleantrueWhether colour fades in as it unrolls.
backSideDarknessnumber0.3How dark the reverse of the sheet is where the roll shows it.
floatStrengthnumber1.0How much it drifts at rest. Held still under reduced motion.
widthSegmentsnumber64Mesh resolution along the roll. Too few and the curl facets.
heightSegmentsnumber32Mesh resolution across the sheet.
getProgress() => number—Reads the unroll amount once per frame instead of taking it as a prop, so a parent can animate it without a React render per frame. Used when hover interaction is off.
antialiasbooleantrueEdge smoothing on its canvas.
dprnumber | [number, number]set from the device tierPixel-density cap.
classNamestring""Applied to the wrapper.
styleReact.CSSProperties{}Inline styles on the wrapper.
mount"auto" | "eager""eager"When to build the poster's scene. The default builds it on mount. Set "auto" to wait until the poster is near the viewport and the page has frame headroom. This is a standalone component with its own gate — PlayPause does not render it and does not pass anything down, so the two settings are independent.
disableAdaptiveQualitybooleanfalseIgnores the device quality tier and renders at full detail. By default the runtime caps the pixel ratio on slower hardware and under frame pressure. It does not turn off reduced-motion handling or take the poster off the shared frame loop.
It reads the visitor's reduced-motion preference and stops the column travelling, which also stops the automatic unrolling — a poster opens because it arrived in the middle, and nothing arrives any more. The posters hold still and hovering one still unrolls it, so every image is reachable. The idle drift on a standalone poster stops too.
The whole ticker is a single drawing context. It used to be one per poster, and because the list is doubled to make the loop seamless, eight posters meant sixteen contexts — the ceiling browsers enforce before they start dropping the oldest and leaving blank canvases. One context also means each image uploads once rather than once per poster. Poster count now costs geometry, which is cheap, and off-screen posters are skipped entirely. The unroll amounts are tweened outside React, so an open poster does not re-render anything.
Client component. It opens a 3D drawing context on mount — import it dynamically with ssr: false if your route renders on the server.
It draws on the shared frame loop rather than starting one of its own, so putting several animated components on a page costs one callback, not one each, and the scheduler can set decorative work aside when frames get tight. The whole strip is drawn in one scene rather than one per poster, so the cost does not grow with the number of items. If the browser takes the drawing context away — which it does under memory pressure, and more often on a page with several of these — the component reports it and rebuilds once the context comes back, instead of leaving a permanently black rectangle.
Back to all components