Layr Components

Components / Scroll scene

Clip Track

A slim clip drops onto a timeline track as its section scrolls in, then a playhead crosses it while you read the section.

Scroll insideFull screen
Props
#E6E5E1
#FF4A1C
#FF4A1C
#E6E5E1

Scroll inside the preview.

In Framer, open Assets, add a new code file, paste this over everything in it and save. Clip Track shows up in the Insert menu, and its controls appear in the right panel.
// Clip Track · Layr Components
//
// Makes a section read like a clip on an edit timeline. As the section scrolls
// into view a slim clip drops onto the track, growing in from the left, then a
// playhead crosses it while the visitor reads on.
// Paste into a new code file in Framer. Put it inside a section (a layer whose
// HTML tag is section), full width and 14 px tall; it follows that section.

import { addPropertyControls, ControlType, useIsStaticRenderer } from "framer"
import { useReducedMotion } from "framer-motion"
import { useEffect, useRef, type CSSProperties } from "react"

interface ClipTrackProps {
    trackColor: string
    clipColor: string
    clipBorder: string
    handleColor: string
    headColor: string
    style?: CSSProperties
}

const clamp = (value: number) => Math.max(0, Math.min(1, value))
/** Ease in and out. */
const smooth = (value: number) => value * value * (3 - 2 * value)

/**
 * @framerIntrinsicWidth 1200
 * @framerIntrinsicHeight 14
 *
 * @framerSupportedLayoutWidth any
 * @framerSupportedLayoutHeight fixed
 */
export default function ClipTrack(props: ClipTrackProps) {
    const {
        trackColor = "rgba(242, 239, 233, 0.14)",
        clipColor = "rgba(233, 255, 161, 0.08)",
        clipBorder = "rgba(242, 239, 233, 0.22)",
        handleColor = "#E9FFA1",
        headColor = "#E9FFA1",
    } = props
    const isStaticRender = useIsStaticRenderer()
    const reduceMotion = useReducedMotion()
    const isStatic = isStaticRender || !!reduceMotion
    const rootRef = useRef<HTMLDivElement>(null)
    const clipRef = useRef<HTMLDivElement>(null)
    const headRef = useRef<HTMLDivElement>(null)

    useEffect(() => {
        const root = rootRef.current
        if (isStatic || !root) return
        const section = (root.closest("section") as HTMLElement | null) ?? root.parentElement
        if (!section) return
        let frame = 0
        const update = () => {
            frame = 0
            const rect = section.getBoundingClientRect()
            const view = window.innerHeight
            // The clip drops in while the section's top travels from the bottom of the screen to 45%.
            const drop = smooth(clamp((view - rect.top) / (view * 0.55)))
            // The playhead crosses the clip while the section scrolls past the middle of the screen.
            const play = clamp((view * 0.5 - rect.top) / Math.max(1, rect.height))
            if (clipRef.current) clipRef.current.style.transform = `scaleX(${drop.toFixed(4)})`
            if (headRef.current) {
                headRef.current.style.left = `${(play * 100).toFixed(3)}%`
                headRef.current.style.opacity = drop > 0.98 ? "1" : "0"
            }
        }
        const schedule = () => {
            if (!frame) frame = requestAnimationFrame(update)
        }
        update()
        window.addEventListener("scroll", schedule, { passive: true })
        window.addEventListener("resize", schedule)
        return () => {
            if (frame) cancelAnimationFrame(frame)
            window.removeEventListener("scroll", schedule)
            window.removeEventListener("resize", schedule)
        }
    }, [isStatic])

    return (
        <div ref={rootRef} aria-hidden="true" style={{ ...props.style, position: "relative", overflow: "visible", pointerEvents: "none" }}>
            <div style={{ position: "absolute", left: 0, right: 0, top: "50%", height: 1, marginTop: -0.5, background: trackColor }} />
            <div
                ref={clipRef}
                style={{
                    position: "absolute",
                    left: 0,
                    right: 0,
                    top: 2,
                    bottom: 2,
                    borderRadius: 3,
                    background: clipColor,
                    border: `1px solid ${clipBorder}`,
                    transformOrigin: "left center",
                    transform: isStatic ? "none" : "scaleX(0)",
                    willChange: "transform",
                }}
            >
                <div style={{ position: "absolute", left: -1, top: -1, bottom: -1, width: 3, borderRadius: "3px 0 0 3px", background: handleColor }} />
                <div style={{ position: "absolute", right: -1, top: -1, bottom: -1, width: 3, borderRadius: "0 3px 3px 0", background: handleColor }} />
            </div>
            <div
                ref={headRef}
                style={{
                    position: "absolute",
                    top: -3,
                    bottom: -3,
                    left: isStatic ? "35%" : 0,
                    width: 2,
                    marginLeft: -1,
                    background: headColor,
                    boxShadow: `0 0 8px ${headColor}`,
                    opacity: isStatic ? 1 : 0,
                }}
            />
        </div>
    )
}

addPropertyControls(ClipTrack, {
    clipColor: { type: ControlType.Color, title: "Clip", defaultValue: "rgba(233, 255, 161, 0.08)" },
    clipBorder: { type: ControlType.Color, title: "Clip Border", defaultValue: "rgba(242, 239, 233, 0.22)" },
    handleColor: { type: ControlType.Color, title: "Handles", defaultValue: "#E9FFA1" },
    headColor: { type: ControlType.Color, title: "Playhead", defaultValue: "#E9FFA1" },
    trackColor: { type: ControlType.Color, title: "Track", defaultValue: "rgba(242, 239, 233, 0.14)" },
})

In this preview

The photos are demo pictures shown on this site only; they are not in the files you copy. Set your own through the image props.