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.