Components / Navigation
Timecode
A running HH:MM:SS:FF timecode for the navigation that plays forward as the page scrolls, with the page's total runtime beside it.
Scroll insideFull screen
Props
#0E0E0D
#6E6E6B
Scroll inside the preview and watch the bar at the top.
In Framer, open Assets, add a new code file, paste this over everything in it and save. Timecode shows up in the Insert menu, and its controls appear in the right panel.
// Timecode · Layr Components
//
// A running timecode, HH:MM:SS:FF, that treats the page like a film: scrolling
// down plays it forward and scrolling up winds it back. The page's total
// runtime can sit beside it.
// Paste into a new code file in Framer. Place it in the navigation; it sizes to its text.
import { addPropertyControls, ControlType, useIsStaticRenderer } from "framer"
import { useEffect, useRef, useState, type CSSProperties } from "react"
interface TimecodeProps {
label: string
showTotal: boolean
fps: number
pixelsPerSecond: number
fontFamily: string
numeralFamily: string
fontSize: number
color: string
mutedColor: string
style?: CSSProperties
}
const MONO = "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"
const pad = (value: number) => String(Math.floor(value)).padStart(2, "0")
/** Seconds → HH:MM:SS:FF, the way an edit timeline shows time. */
function toTimecode(seconds: number, fps: number) {
const safe = Math.max(0, seconds)
const hours = safe / 3600
const minutes = (safe % 3600) / 60
const secs = safe % 60
const frames = (safe - Math.floor(safe)) * fps
return `${pad(hours)}:${pad(minutes)}:${pad(secs)}:${pad(frames)}`
}
/**
* @framerIntrinsicWidth 220
* @framerIntrinsicHeight 16
*
* @framerSupportedLayoutWidth auto
* @framerSupportedLayoutHeight auto
*/
export default function Timecode(props: TimecodeProps) {
const {
label = "Time",
showTotal = true,
fps = 24,
pixelsPerSecond = 120,
fontFamily = "Inter",
numeralFamily = "",
fontSize = 12,
color = "#F2EFE9",
mutedColor = "#8F8C86",
} = props
const isStatic = useIsStaticRenderer()
const currentRef = useRef<HTMLSpanElement>(null)
const [total, setTotal] = useState(toTimecode(144, fps))
useEffect(() => {
if (isStatic) return
let frame = 0
const update = () => {
frame = 0
const max = Math.max(0, document.documentElement.scrollHeight - window.innerHeight)
if (currentRef.current) currentRef.current.textContent = toTimecode(window.scrollY / pixelsPerSecond, fps)
const next = toTimecode(max / pixelsPerSecond, fps)
setTotal((previous) => (previous === next ? previous : next))
}
const schedule = () => {
if (!frame) frame = requestAnimationFrame(update)
}
update()
window.addEventListener("scroll", schedule, { passive: true })
window.addEventListener("resize", schedule)
const observer = new ResizeObserver(schedule)
observer.observe(document.body)
return () => {
if (frame) cancelAnimationFrame(frame)
window.removeEventListener("scroll", schedule)
window.removeEventListener("resize", schedule)
observer.disconnect()
}
}, [isStatic, fps, pixelsPerSecond])
const numerals: CSSProperties = { fontFamily: numeralFamily ? `"${numeralFamily}", ${MONO}` : MONO, letterSpacing: "0.04em" }
return (
<div
style={{
...props.style,
display: "flex",
alignItems: "center",
gap: "0.6em",
whiteSpace: "nowrap",
fontVariantNumeric: "tabular-nums",
userSelect: "none",
fontFamily: `"${fontFamily}", "Inter", system-ui, sans-serif`,
fontSize,
lineHeight: 1,
color,
}}
>
{label && <span style={{ color: mutedColor }}>{label}</span>}
<span ref={currentRef} style={numerals}>
{toTimecode(isStatic ? 12.34 : 0, fps)}
</span>
{showTotal && <span style={{ ...numerals, color: mutedColor }}>/ {total}</span>}
</div>
)
}
addPropertyControls(Timecode, {
label: { type: ControlType.String, title: "Label", defaultValue: "Time" },
showTotal: { type: ControlType.Boolean, title: "Runtime", defaultValue: true, enabledTitle: "Show", disabledTitle: "Hide" },
fps: { type: ControlType.Number, title: "FPS", defaultValue: 24, min: 12, max: 60, step: 1 },
pixelsPerSecond: {
type: ControlType.Number,
title: "Px / Second",
defaultValue: 120,
min: 20,
max: 1000,
step: 10,
description: "How many pixels of scrolling count as one second of film.",
},
fontFamily: { type: ControlType.String, title: "Font family", defaultValue: "Inter", description: "For the label. Use the family name of your text styles." },
numeralFamily: {
type: ControlType.String,
title: "Numeral font",
defaultValue: "",
placeholder: "System monospace",
description: "For the numbers. Leave empty for the system monospace, so the digits never jump.",
},
fontSize: { type: ControlType.Number, title: "Size", defaultValue: 12, min: 9, max: 40, step: 1, unit: "px" },
color: { type: ControlType.Color, title: "Color", defaultValue: "#F2EFE9" },
mutedColor: { type: ControlType.Color, title: "Muted", defaultValue: "#8F8C86" },
})
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.