Components / Text
Lit Text
A large statement whose words turn from quiet grey to ink one by one as it moves up the window; the opening words stay dark.
Scroll insideFull screen
Props
#0E0E0D
#A3A3A0
Scroll inside the preview.
In Framer, open Assets, add a new code file, paste this over everything in it and save. Lit Text shows up in the Insert menu, and its controls appear in the right panel.
// Lit Text · Layr Components
//
// A large statement that is read as you scroll. The opening words are always in
// ink; the rest start quiet and turn to ink word by word as the paragraph moves
// up the window. Without JavaScript every word shows in ink.
// Paste into a new code file in Framer. Place it in the page flow with a fixed
// width; its height follows the text.
import { addPropertyControls, ControlType, useIsStaticRenderer } from "framer"
import { createElement, useEffect, useRef, type CSSProperties } from "react"
interface LitTextProps {
lead: string
text: string
tag: string
textSize: number
lineHeight: number
tracking: number
weight: number
fontFamily: string
ink: string
quiet: string
style?: CSSProperties
}
const STYLES = `
.lyr-lt{margin:0;-webkit-font-smoothing:antialiased;font-synthesis:none;text-wrap:pretty;color:var(--lyr-lt-ink)}
.lyr-lt-w{transition:color .45s ease}
.lyr-lt-w[data-lit="0"]{color:var(--lyr-lt-quiet)}
@media (prefers-reduced-motion:reduce){.lyr-lt-w{transition:none}}
`
/**
* @framerIntrinsicWidth 1000
* @framerIntrinsicHeight 200
*
* @framerSupportedLayoutWidth fixed
* @framerSupportedLayoutHeight auto
*/
export default function LitText(props: LitTextProps) {
const {
lead = "Read it as you scroll.",
text = "The opening words stay dark. The rest turn from quiet grey to ink, one by one, as the paragraph moves up the window.",
tag = "h2",
textSize = 64,
lineHeight = 1.02,
tracking = -0.042,
weight = 400,
fontFamily = "Inter",
ink = "#1C1B1A",
quiet = "#A09D97",
style,
} = props
const isStatic = useIsStaticRenderer()
const ref = useRef<HTMLElement>(null)
const words = String(text).split(/\s+/).filter(Boolean)
useEffect(() => {
const el = ref.current
if (!el || isStatic || typeof window === "undefined") return
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return
const spans = Array.from(el.querySelectorAll<HTMLElement>(".lyr-lt-w"))
let lit = spans.length
let raf = 0
const read = () => {
raf = 0
const box = el.getBoundingClientRect()
const vh = window.innerHeight
const start = vh * 0.82
const end = vh * 0.38
const p = Math.max(0, Math.min(1, (start - box.top) / (start - end + box.height * 0.6)))
const next = Math.round(p * spans.length)
if (next === lit) return
const from = Math.min(lit, next)
const to = Math.max(lit, next)
for (let i = from; i < to; i++) spans[i].dataset.lit = next > lit ? "1" : "0"
lit = next
}
const request = () => {
if (!raf) raf = requestAnimationFrame(read)
}
window.addEventListener("scroll", request, { passive: true })
window.addEventListener("resize", request)
read()
return () => {
window.removeEventListener("scroll", request)
window.removeEventListener("resize", request)
if (raf) cancelAnimationFrame(raf)
}
}, [text, isStatic])
const t = String(tag).toLowerCase()
const tagName = t.includes("h1") ? "h1" : t.includes("h3") ? "h3" : t.includes("p") ? "p" : "h2"
const full = `${lead} ${text}`.trim()
return createElement(
tagName,
{
ref,
className: "lyr-lt",
"aria-label": full,
style: {
...style,
fontFamily: `"${fontFamily}", "Inter", system-ui, sans-serif`,
fontSize: textSize,
lineHeight,
letterSpacing: `${tracking}em`,
fontWeight: weight,
"--lyr-lt-ink": ink,
"--lyr-lt-quiet": quiet,
} as CSSProperties,
},
<style key="s" dangerouslySetInnerHTML={{ __html: STYLES }} />,
<span key="l" aria-hidden="true">
{lead}{" "}
</span>,
...words.map((w, i) => (
<span key={i} className="lyr-lt-w" data-lit="1" aria-hidden="true">
{w}
{i < words.length - 1 ? " " : ""}
</span>
))
)
}
addPropertyControls(LitText, {
lead: { type: ControlType.String, title: "Opening", defaultValue: "Read it as you scroll.", description: "Always in ink." },
text: {
type: ControlType.String,
title: "Text",
defaultValue: "The opening words stay dark. The rest turn from quiet grey to ink, one by one, as the paragraph moves up the window.",
displayTextArea: true,
description: "Turns from quiet to ink as you scroll.",
},
tag: { type: ControlType.Enum, title: "Tag", options: ["h1", "h2", "h3", "p"], optionTitles: ["H1", "H2", "H3", "P"], defaultValue: "h2" },
textSize: { type: ControlType.Number, title: "Text size", defaultValue: 64, min: 16, max: 200, step: 1, unit: "px" },
lineHeight: { type: ControlType.Number, title: "Line height", defaultValue: 1.02, min: 0.8, max: 2, step: 0.01 },
tracking: { type: ControlType.Number, title: "Letter spacing", defaultValue: -0.042, min: -0.1, max: 0.1, step: 0.001, unit: "em" },
weight: { type: ControlType.Number, title: "Weight", defaultValue: 400, min: 200, max: 700, step: 100 },
fontFamily: { type: ControlType.String, title: "Font family", defaultValue: "Inter", description: "Use the family name of your text styles." },
ink: { type: ControlType.Color, title: "Ink", defaultValue: "#1C1B1A" },
quiet: { type: ControlType.Color, title: "Quiet", defaultValue: "#A09D97" },
})
In this preview
No pictures needed.