Scroll Trigger

Reveals the text, images and groups inside it in order as it scrolls into view, or ties their reveal to the scroll position.

<tp-scroll-trigger>
Scroll Trigger
Default density Compact density Restart from the initial state Show the copyable source

Quiet spaces, open light

Three rooms designed for focused work.

Usage

A Scroll Trigger coordinates the revealing components inside it: Text Motion, Image, Image Group and nested triggers. As it nears the viewport it asks every member to prepare, so images start loading. Once it is in view and everything is ready, it reveals the members in document order, stagger milliseconds apart. Here the heading waits for its fonts and the caption waits for all three images. The trigger itself is an unstyled block, so lay out its content as you like.

A group or trigger inside the trigger is a single member: it is ready when all of its own members are, and it plays its own sequence after the delay it receives. Members never reveal by themselves inside a trigger. A member added after the sequence reveals as soon as it is ready. It is a block before it is defined too, so binding it never moves the content around it.

The trigger prepares members within 25% of the visible area (and of scroll containers), and reveals on any intersection. Without IntersectionObserver, or under reduced motion, the sequence plays as soon as the members are ready, without stagger.

Stagger order

Members without their own reveal use the trigger’s. stagger-from starts the sequence from the first, last or center member.

From the center
Default density Compact density Restart from the initial state Show the copyable source

Plan

Design

Build

Review

Ship

Replay

reveal-repeat resets every member once the trigger is fully out of view and plays the sequence again on the way back. Use it sparingly, for sections people return to. With reveal-repeat it reveals only once it is 10% inside the viewport and resets the moment it is entirely out, so a member’s start offset cannot pull it back in and loop.

Replay on entry
Default density Compact density Restart from the initial state Show the copyable source

Scroll down, then back up.

Every release, reviewed by a human.

Two approvals before anything reaches production.

Scrubbing

scrub maps the scroll position onto the same choreography a timed reveal would play, in both directions. scrub-range picks the stretch of scroll that drives it (contain, cover, entry, exit, or offsets like entry 25% contain 40%), and scrub-smoothing lets progress trail the scroll slightly. reveal-repeat has no effect while scrubbing.

Scrubbed band
Default density Compact density Restart from the initial state Show the copyable source

The first line slides in with the scroll,

then the second,

and back out as you scroll up.

Offsets work like CSS animation-range. With scrub-range="entry 25% contain 40%" the reveal starts once a quarter of the section has entered and is finished well before it reaches the middle of the frame, so it sits still while people read it. Add scrub-once to leave revealed content at rest when scrolling back. A start without a percentage means 0% of its range and an end without one 100% (entry exit runs from starting to enter to finishing leaving); a single point such as cover 50% runs from there to the end of its range. A value that cannot be parsed falls back to contain.

Members are still asked to prepare when the trigger nears the viewport, but scrubbing does not wait for them: an image that is still loading scrubs in with its placeholder. Under reduced motion every member is at rest and progress is still reported. Turning scrub off returns the members to timed reveals.

Custom scroll range
Default density Compact density Restart from the initial state Show the copyable source

Book a room in two taps

Pinning

pin holds the content in a sticky stage while the scroll advances through an extra length, --tp-scroll-trigger-pin-length. With scrub, that is the classic pinned scene. Give the scroll container container-type: size so the stage fills it exactly; without one, the stage is the viewport.

Pinned scene
Default density Compact density Restart from the initial state Show the copyable source

Scroll inside this frame.

Every metric, one calm dashboard

Revenue, retention and usage stay pinned while you read.

Scroll back up to play it in reverse.

The stage is one visible extent tall (minus --tp-scroll-trigger-pin-top, for fixed headers) and clips what does not fit, so lay the scene out inside it as above. The track has the same height before the element is defined (from @tweakpad/ui/styles.css), so pinning never moves the content that follows. Pinning also works without scrub, for a timed reveal inside a pinned stage. As for any sticky element, there must be no clipping ancestor between the trigger and its scroller.

Events

tp-reveal-change fires when the sequence starts and tp-reveal-change-complete when the last member settles. tp-scroll-progress reports scrubbed progress each frame, for driving your own animation. Members’ events bubble under the same names, so check event.target. To sync other motion, set reveal-hold, wait for what you need, then clear it.

Reveal events
Default density Compact density Restart from the initial state Show the copyable source

Welcome to your workspace

Your projects, people and files in one place.

Waiting

API Reference

Every element also accepts motion-policy.

Attributes

AttributePropertyTypeDefaultDescriptionstaggerstaggernumber0Milliseconds between consecutive member reveals; 0 reveals them together.stagger-fromstaggerFromStaggerFrom'first'The member the stagger starts from.revealrevealstring''Default reveal effect for members without their own.reveal-repeatrevealRepeatbooleanfalseReplay the sequence each time the trigger re-enters view.reveal-holdrevealHoldbooleanfalseWhile set (or while any member holds), every member waits in its start state.scrubscrubbooleanfalseReveal with the scroll position instead of over time.scrub-rangescrubRangestring'contain'The scroll range mapped to progress while scrubbing: contain, cover, entry or exit, or, as in CSS animation-range, a start and an end point such as entry 20% contain 50%.scrub-smoothingscrubSmoothingnumber0How far scrubbed progress trails the scroll, 0 through 0.98; 0 is locked to it.scrub-oncescrubOncebooleanfalseScrubbed progress only moves forward, leaving revealed members at rest on scroll back.pinpinbooleanfalsePin the content in a sticky stage for an extra scroll length.

Properties

PropertyTypeDefaultDescriptionstatusread-onlyScrollTriggerStatus—Aggregate readiness of the members.membersread-onlyHTMLElement[]—The member elements, in document order.revealedread-onlyboolean—Whether the sequence has played (false again after a repeat reset).progressread-onlynumber—Scrubbed progress, 0 through 1 (0 when not scrubbing).

Events

EventTypeDescriptiontp-scroll-progressCustomEvent<{ progress: number; }>{ progress } in each frame scrubbed progress changes.tp-loading-status-changeCustomEvent<{ status: "idle" | "loading" | "ready"; ready: number; failed: number; total: number; }>{ status, ready, failed, total } when the aggregate readiness changes.tp-reveal-changeCustomEvent<{ revealed: boolean; }>{ revealed } when the sequence starts or a repeat resets it.tp-reveal-change-completeCustomEvent<{ revealed: boolean; }>{ revealed } after the last member's reveal settles.

Slots

SlotDescriptiondefaultContent, including the members at any depth.

CSS parts

PartDescription::part(stage)The wrapper of the content, which sticks in place while pinned.

CSS custom properties

PropertyDefaultDescription--tp-scroll-trigger-pin-length—Extra scroll length while pinned. Default 200cqb (twice the visible extent: the nearest size container, otherwise the viewport).--tp-scroll-trigger-pin-top—Pinned offset from the viewport start. Default 0.