Layr Components

Components / Hero

Rack Focus

A hero of photos at different depths: one sharp, the rest blurred, and scrolling pulls focus from print to print along a lens distance scale.

Scroll insideFull screen
Props
55 vh
84 px
0
#0E0E0D
#6E6E6B
#E6E5E1
#FF4A1C
#FFFFFF

Set in code: Items. The preview fills these in with demo content.

Scroll inside the preview, or click a stop on the scale.

In Framer, open Assets, add a new code file, paste this over everything in it and save. Rack Focus shows up in the Insert menu, and its controls appear in the right panel.
// Rack Focus · Layr Components
//
// A hero of photos hung at different depths. One print is sharp and the rest
// are out of focus, nearer or further away; scrolling pulls focus from one
// print to the next. Under it sit a heading and a lens distance scale whose
// marker follows the scroll; each stop on the scale jumps to its photo.
// Paste into a new code file in Framer. Place it at the top of the page at
// full width; it is taller than the window and pins itself while focus moves.
// No ancestor may clip overflow. Without photos it shows plain tones.

import { addPropertyControls, ControlType, useIsStaticRenderer } from "framer"
import { useEffect, useRef, useState, startTransition, type CSSProperties } from "react"

type Img = { src: string; srcSet?: string; alt?: string }
type Item = { image?: Img; title?: string; detail?: string; note?: string; link?: string }

interface RackFocusProps {
    items: Item[]
    heading: string
    subheading: string
    hint: string
    linkLabel: string
    scale: boolean
    shape: string
    travel: number
    topSpace: number
    imageWidth: string
    preview: number
    fontFamily: string
    ink: string
    muted: string
    line: string
    accent: string
    background: string
    style?: CSSProperties
}

const ITEMS: Item[] = [
    { title: "Low tide", detail: "North coast", note: "2026" },
    { title: "Dry season", detail: "Inland", note: "2026" },
    { title: "Evening light", detail: "Hills", note: "2025" },
    { title: "Harbour, early", detail: "West coast", note: "2025" },
    { title: "Quiet rooms", detail: "Studio", note: "2024" },
    { title: "Pale morning", detail: "Dunes", note: "2024" },
]

// Shown when a print has no photo yet.
const EMPTY = ["linear-gradient(160deg, #6f7a7d, #2f3537)", "linear-gradient(160deg, #8a7a66, #3d342b)", "linear-gradient(160deg, #5f5a66, #28252c)", "linear-gradient(160deg, #7d7a70, #3a3832)"]

const clamp = (v: number, a: number, b: number) => Math.min(b, Math.max(a, v))
const two = (n: number) => String(n).padStart(2, "0")

// Hold each print for a while, then pull focus to the next one.
function plateau(raw: number): number {
    const i = Math.floor(raw)
    const f = raw - i
    const hold = 0.2
    const u = clamp((f - hold) / (1 - 2 * hold), 0, 1)
    return i + u * u * (3 - 2 * u)
}

// Where a print sits for each depth step. 0 is the print in focus, negative
// steps are nearer than the focus plane, positive steps are further away.
// x and y are percentages of the stage, measured from its centre.
const DEPTH = [-2, -1, 0, 1, 2, 3]
const WIDE = {
    x: [96, 52, 4, -25, -38, -44],
    y: [88, 46, 0, -24, -6, -30],
    s: [1.7, 1.25, 1, 0.4, 0.25, 0.15],
}
const TALL = {
    x: [56, 30, 0, -18, 22, -8],
    y: [130, 74, 0, -41, -50, -62],
    s: [1.45, 1.15, 1, 0.44, 0.27, 0.15],
}
const FADE = [0, 0.86, 1, 0.62, 0.42, 0]

function sample(values: number[], d: number, log = false): number {
    const u = clamp(d, DEPTH[0], DEPTH[DEPTH.length - 1]) - DEPTH[0]
    const i = Math.min(values.length - 2, Math.floor(u))
    const f = u - i
    if (log) return Math.exp(Math.log(values[i]) + (Math.log(values[i + 1]) - Math.log(values[i])) * f)
    return values[i] + (values[i + 1] - values[i]) * f
}

// Depth of item i when the focus is at position p. The list wraps, so there
// is always something nearer and something further than the print in focus.
function depthOf(i: number, p: number, n: number): number {
    if (n < 3) return i - p
    const half = n / 2
    return ((((i - p + half) % n) + n) % n) - half
}

function printVars(d: number, n: number): Record<string, string> {
    const edge = n < 3 ? 1 : clamp((n / 2 - Math.abs(d)) / 0.5, 0, 1)
    const k0 = clamp(1 - Math.abs(d) / 0.6, 0, 1)
    const k = k0 * k0 * (3 - 2 * k0)
    return {
        "--x": sample(WIDE.x, d).toFixed(3),
        "--y": sample(WIDE.y, d).toFixed(3),
        "--s": sample(WIDE.s, d, true).toFixed(4),
        "--xt": sample(TALL.x, d).toFixed(3),
        "--yt": sample(TALL.y, d).toFixed(3),
        "--st": sample(TALL.s, d, true).toFixed(4),
        "--o": (sample(FADE, d) * edge).toFixed(3),
        "--k": k.toFixed(3),
        "--bo": (1 - Math.pow(k, 4)).toFixed(3),
        "--z": String(Math.round(100 - d * 10)),
    }
}

// Focus distances as engraved on a lens barrel, in metres. The last stop is
// always infinity. Spacing follows a real lens: wide apart close up, tight
// towards infinity. Past eight photos the stops are numbered.
const DISTANCES: Record<number, number[]> = {
    2: [0.7, Infinity],
    3: [0.7, 2, Infinity],
    4: [0.7, 1.5, 4, Infinity],
    5: [0.7, 1.2, 2, 5, Infinity],
    6: [0.7, 1, 1.5, 3, 7, Infinity],
    7: [0.7, 1, 1.5, 2, 3, 7, Infinity],
    8: [0.7, 1, 1.5, 2, 3, 5, 10, Infinity],
}

function stops(n: number): { at: number; label: string }[] {
    const d = DISTANCES[n]
    if (!d) return Array.from({ length: n }, (_, i) => ({ at: n > 1 ? (i / (n - 1)) * 100 : 0, label: String(i + 1) }))
    return d.map((v, i) => ({
        at: v === Infinity ? 100 : (1 - d[0] / v) * 100,
        label: v === Infinity ? "∞" : i === 0 ? `${v} m` : String(v),
    }))
}

function markAt(marks: { at: number }[], p: number): number {
    if (!marks.length) return 0
    const max = marks.length - 1
    if (max === 0) return marks[0].at
    const c = Math.min(max, Math.max(0, p))
    const i = Math.min(max - 1, Math.floor(c))
    return marks[i].at + (marks[i + 1].at - marks[i].at) * (c - i)
}

function ratioOf(shape: string): number {
    const s = String(shape).toLowerCase()
    if (s.includes("4:5") || s.includes("portrait")) return 0.8
    if (s.includes("1:1") || s.includes("square")) return 1
    if (s.includes("16:9") || s.includes("wide")) return 16 / 9
    return 1.5
}

const SHEET = `
.lyr-rf{position:relative;width:100%;background:var(--ground);color:var(--ink)}
.lyr-rf-pin{position:sticky;top:0;width:100%;height:100vh;height:100dvh;min-height:520px;box-sizing:border-box;padding-top:var(--top);display:flex;flex-direction:column;overflow:clip}
.lyr-rf[data-static="1"] .lyr-rf-pin{position:relative;height:760px}
.lyr-rf-stage{position:relative;flex:1 1 0;min-height:160px;padding:0 40px}
.lyr-rf-box{position:relative;width:100%;height:100%;container-type:size;isolation:isolate}
.lyr-rf-in{position:absolute;inset:0;--pw:min(62cqw,calc((100cqh - 64px)*var(--ratio)));--blur:max(7px,1.15cqw)}
.lyr-rf-p{position:absolute;left:50%;top:calc(50% - 22px);width:var(--pw);aspect-ratio:var(--ratio);display:block;border-radius:2px;text-decoration:none;pointer-events:none;opacity:var(--o);z-index:var(--z);will-change:transform,opacity;transform:translate3d(calc(-50% + var(--x)*1cqw),calc(-50% + var(--y)*1cqh),0) scale(var(--s))}
.lyr-rf-p[data-on="1"]{pointer-events:auto}
.lyr-rf-p img,.lyr-rf-p .lyr-rf-t{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;border-radius:inherit;user-select:none;-webkit-user-drag:none}
.lyr-rf-b{filter:blur(var(--blur));opacity:var(--bo)}
.lyr-rf-s{opacity:var(--k)}
.lyr-rf-e{position:absolute;inset:0;border-radius:2px;background:rgba(255,255,255,.07)}
.lyr-rf-cap{position:absolute;left:calc(50% + 4cqw);top:calc(50% - 22px + var(--pw)/var(--ratio)/2 + 14px);width:var(--pw);transform:translateX(-50%);display:flex;justify-content:space-between;align-items:baseline;gap:16px;font-size:14px;line-height:1.3;letter-spacing:-.005em;font-weight:400;z-index:200;font-variant-numeric:tabular-nums}
.lyr-rf-cap b{font-weight:500}
.lyr-rf-cap a{color:inherit;text-decoration:none}
.lyr-rf-cap a:hover{opacity:.68}
.lyr-rf-row{display:flex;gap:8px;min-width:0;animation:lyr-rf-in .28s ease both}
.lyr-rf-row span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--muted)}
.lyr-rf-go{white-space:nowrap;display:inline-flex;align-items:center;gap:6px;transition:opacity .2s}
.lyr-rf-count{white-space:nowrap;color:var(--muted)}
.lyr-rf-foot{position:relative;z-index:2;padding:0 40px 26px;background:linear-gradient(0deg,var(--ground) 0%,color-mix(in srgb,var(--ground) 86%,transparent) 34%,color-mix(in srgb,var(--ground) 0%,transparent) 100%)}
.lyr-rf-head{display:flex;align-items:flex-end;gap:24px;padding:18px 0 26px}
.lyr-rf-h{flex:1 1 auto;min-width:0;margin:0;font-size:clamp(40px,calc(4.4vw + 22px),78px);font-weight:400;line-height:1;letter-spacing:-.04em;text-wrap:balance}
.lyr-rf-h span{display:block}
.lyr-rf-h span+span{color:var(--muted)}
.lyr-rf-hint{flex:none;margin:0;font-size:14px;line-height:1.45;letter-spacing:-.005em;color:var(--muted);white-space:nowrap}
.lyr-rf-sc{position:relative;width:100%;height:46px;container-type:inline-size;font-size:13px;line-height:1;letter-spacing:-.005em;font-variant-numeric:tabular-nums;color:var(--muted)}
.lyr-rf-rule{position:absolute;left:0;right:0;top:14px;height:1px;background:var(--line)}
.lyr-rf-stop{position:absolute;top:0;height:46px;padding:0;margin:0;border:0;background:none;color:inherit;font:inherit;cursor:pointer;transform:translateX(-50%);display:flex;flex-direction:column;align-items:center;transition:color .25s}
.lyr-rf-stop i{display:block;width:1px;height:9px;margin-top:10px;opacity:.75;background:currentColor}
.lyr-rf-stop span{margin-top:9px;white-space:nowrap}
.lyr-rf-stop:first-of-type{transform:none;align-items:flex-start}
.lyr-rf-stop:last-of-type{transform:translateX(-100%);align-items:flex-end}
.lyr-rf-stop:hover span{opacity:.68}
.lyr-rf-stop:focus-visible{outline:1px solid currentColor;outline-offset:4px}
.lyr-rf-mark{position:absolute;left:0;top:2px;width:1px;height:17px;background:var(--accent);transform:translateX(calc(var(--m)*1cqw - .5px));will-change:transform}
@keyframes lyr-rf-in{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
@container (max-aspect-ratio:13/10){
.lyr-rf-in{--pw:min(78cqw,calc((100cqh - 64px)*var(--ratio)))}
}
@container (max-aspect-ratio:1/1){
.lyr-rf-in{--pw:min(92cqw,calc((100cqh - 64px)*var(--ratio)))}
.lyr-rf-p{transform:translate3d(calc(-50% + var(--xt)*1cqw),calc(-50% + var(--yt)*1cqh),0) scale(var(--st))}
.lyr-rf-cap{left:50%}
}
@media (max-width:600px){
.lyr-rf-pin{padding-top:calc(var(--top) * .76);min-height:480px}
.lyr-rf-stage{padding:0 16px}
.lyr-rf-foot{padding:0 16px 16px}
.lyr-rf-head{padding:12px 0 18px}
.lyr-rf-hint{display:none}
}
@media (prefers-reduced-motion:reduce){.lyr-rf-row{animation:none}}
`

/**
 * @framerIntrinsicWidth 1200
 * @framerIntrinsicHeight 760
 *
 * @framerSupportedLayoutWidth fixed
 * @framerSupportedLayoutHeight auto
 */
export default function RackFocus(props: RackFocusProps) {
    const {
        items = ITEMS,
        heading = "Places, people",
        subheading = "and the light between.",
        hint = "Scroll to pull focus",
        linkLabel = "View project",
        scale = true,
        shape = "3:2 landscape",
        travel = 55,
        topSpace = 84,
        imageWidth = "62vw",
        preview = 0,
        fontFamily = "Inter",
        ink = "#ECE9E2",
        muted = "#8E8B84",
        line = "rgba(236,233,226,0.38)",
        accent = "#F0C445",
        background = "#0A0A0A",
    } = props
    const isStatic = useIsStaticRenderer()
    const list = (items || []).filter((item) => item && (item.title || item.image?.src))
    const [active, setActive] = useState(0)
    const root = useRef<HTMLDivElement>(null)
    const prints = useRef<(HTMLAnchorElement | null)[]>([])
    const mark = useRef<HTMLDivElement>(null)
    const pRef = useRef(0)
    const activeRef = useRef(0)
    const jump = useRef<(i: number) => void>(() => {})

    const n = list.length
    const marks = stops(n)

    // The scroll track: how far the tall root has been scrolled, held at each
    // print for a moment and eased between them.
    useEffect(() => {
        const host = root.current
        if (!host || isStatic || n < 1) return
        const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches
        const scaleMarks = stops(n)
        let raf = 0
        let last = 0
        let lastScroll = 0
        let top = 0
        let range = 0
        let t = 0

        const paint = (p: number) => {
            pRef.current = p
            for (let i = 0; i < n; i++) {
                const el = prints.current[i]
                if (!el) continue
                const d = depthOf(i, p, n)
                const vars = printVars(d, n)
                for (const name in vars) el.style.setProperty(name, vars[name])
                el.dataset.on = Math.abs(d) < 0.5 ? "1" : "0"
            }
            if (mark.current) mark.current.style.setProperty("--m", markAt(scaleMarks, p).toFixed(3))
            const now = ((Math.round(p) % n) + n) % n
            if (now !== activeRef.current) {
                activeRef.current = now
                startTransition(() => setActive(now))
            }
        }

        const measure = () => {
            const r = host.getBoundingClientRect()
            range = r.height - window.innerHeight
            top = r.top + window.scrollY
            t = range > 1 ? clamp(-r.top / range, 0, 1) : 0
        }

        const tick = (now: number) => {
            raf = 0
            const idle = now - lastScroll > 170
            let goal = plateau(t * (n - 1))
            if (idle || reduced) goal = Math.round(goal)
            const dt = Math.min(64, Math.max(1, now - last))
            last = now
            let p = reduced ? goal : pRef.current + (goal - pRef.current) * (1 - Math.exp(-dt / 140))
            if (Math.abs(goal - p) < 0.0004) p = goal
            if (p !== pRef.current) paint(p)
            if (p !== goal || !idle) raf = requestAnimationFrame(tick)
        }

        const wake = () => {
            if (!raf) {
                last = performance.now()
                raf = requestAnimationFrame(tick)
            }
        }

        const onScroll = () => {
            const before = t
            measure()
            if (t !== before) {
                lastScroll = performance.now()
                wake()
            }
        }

        const onResize = () => {
            measure()
            wake()
        }

        jump.current = (index: number) => {
            measure()
            window.scrollTo({ top: top + (n > 1 ? index / (n - 1) : 0) * Math.max(0, range), behavior: reduced ? "auto" : "smooth" })
        }

        measure()
        paint(Math.round(t * (n - 1)))
        window.addEventListener("scroll", onScroll, { passive: true })
        window.addEventListener("resize", onResize)
        return () => {
            window.removeEventListener("scroll", onScroll)
            window.removeEventListener("resize", onResize)
            if (raf) cancelAnimationFrame(raf)
            jump.current = () => {}
        }
    }, [n, isStatic])

    const p = isStatic ? clamp(preview, 0, Math.max(0, n - 1)) : pRef.current
    const shown = isStatic ? Math.round(p) : Math.min(active, Math.max(0, n - 1))
    const ratio = ratioOf(shape)
    const current = list[shown]
    const font = `"${fontFamily}", "Inter", system-ui, sans-serif`
    const height = isStatic ? undefined : `calc(100vh + ${Math.max(0, n - 1) * travel}vh)`
    const vars = { "--ink": ink, "--muted": muted, "--line": line, "--accent": accent, "--ground": background, "--top": `${topSpace}px` } as CSSProperties
    const hasHead = Boolean(heading || subheading || hint)

    return (
        <div ref={root} className="lyr-rf" data-static={isStatic ? "1" : "0"} style={{ ...props.style, ...vars, height, fontFamily: font }}>
            <style dangerouslySetInnerHTML={{ __html: SHEET }} />
            <div className="lyr-rf-pin">
                <div className="lyr-rf-stage">
                    <div className="lyr-rf-box">
                        <div className="lyr-rf-in" style={{ "--ratio": String(ratio) } as CSSProperties}>
                            {n === 0 &&
                                [1, 0, -1].map((d) => (
                                    <div key={d} className="lyr-rf-p" style={printVars(d, 3) as CSSProperties}>
                                        <div className="lyr-rf-e" />
                                    </div>
                                ))}
                            {list.map((item, i) => {
                                const d = depthOf(i, p, n)
                                const img = item.image
                                const tone = { background: EMPTY[i % EMPTY.length] }
                                return (
                                    <a
                                        key={i}
                                        ref={(el) => {
                                            prints.current[i] = el
                                        }}
                                        className="lyr-rf-p"
                                        href={item.link || undefined}
                                        data-on={Math.abs(d) < 0.5 ? "1" : "0"}
                                        tabIndex={-1}
                                        aria-hidden="true"
                                        style={printVars(d, n) as CSSProperties}
                                    >
                                        {img?.src ? (
                                            <>
                                                <img className="lyr-rf-b" srcSet={img.srcSet} sizes={imageWidth} src={img.src} alt="" loading={i === 0 ? "eager" : "lazy"} decoding="async" draggable={false} />
                                                <img className="lyr-rf-s" srcSet={img.srcSet} sizes={imageWidth} src={img.src} alt={img.alt || item.title || ""} loading={i === 0 ? "eager" : "lazy"} decoding="async" draggable={false} />
                                            </>
                                        ) : (
                                            <>
                                                <span className="lyr-rf-t lyr-rf-b" style={tone} />
                                                <span className="lyr-rf-t lyr-rf-s" style={tone} />
                                            </>
                                        )}
                                    </a>
                                )
                            })}
                            {current && (
                                <div className="lyr-rf-cap">
                                    <div className="lyr-rf-row" key={shown}>
                                        <b>{current.title}</b>
                                        <span>{[current.detail, current.note].filter(Boolean).join(", ")}</span>
                                    </div>
                                    {current.link ? (
                                        <a className="lyr-rf-go" href={current.link}>
                                            {linkLabel}
                                            <svg width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.3" aria-hidden="true">
                                                <path d="M3 8h10M9 4l4 4-4 4" />
                                            </svg>
                                        </a>
                                    ) : (
                                        <span className="lyr-rf-count">
                                            {two(shown + 1)} of {two(n)}
                                        </span>
                                    )}
                                </div>
                            )}
                        </div>
                    </div>
                </div>
                {(hasHead || (scale && n > 0)) && (
                    <div className="lyr-rf-foot">
                        {hasHead && (
                            <div className="lyr-rf-head">
                                <h2 className="lyr-rf-h">
                                    {heading && <span>{heading}</span>}
                                    {subheading && <span>{subheading}</span>}
                                </h2>
                                {hint && <p className="lyr-rf-hint">{hint}</p>}
                            </div>
                        )}
                        {scale && n > 0 && (
                            <div className="lyr-rf-sc">
                                <div className="lyr-rf-rule" />
                                {marks.map((m, i) => (
                                    <button
                                        key={i}
                                        type="button"
                                        className="lyr-rf-stop"
                                        style={{ left: `${m.at}%`, color: i === shown ? accent : undefined }}
                                        aria-label={`Show ${list[i]?.title || "photo " + (i + 1)}`}
                                        aria-current={i === shown ? "true" : undefined}
                                        onClick={() => jump.current(i)}
                                    >
                                        <i />
                                        <span>{m.label}</span>
                                    </button>
                                ))}
                                <div ref={mark} className="lyr-rf-mark" style={{ "--m": markAt(marks, p).toFixed(3) } as CSSProperties} />
                            </div>
                        )}
                    </div>
                )}
            </div>
        </div>
    )
}

addPropertyControls(RackFocus, {
    items: {
        type: ControlType.Array,
        title: "Items",
        maxCount: 12,
        control: {
            type: ControlType.Object,
            controls: {
                image: { type: ControlType.ResponsiveImage, title: "Photo" },
                title: { type: ControlType.String, title: "Title", defaultValue: "" },
                detail: { type: ControlType.String, title: "Detail", defaultValue: "" },
                note: { type: ControlType.String, title: "Note", defaultValue: "" },
                link: { type: ControlType.Link, title: "Link" },
            },
        },
        defaultValue: ITEMS,
        description: "One print per item, in the order focus moves through them.",
    },
    heading: { type: ControlType.String, title: "Heading", defaultValue: "Places, people" },
    subheading: { type: ControlType.String, title: "Second line", defaultValue: "and the light between.", description: "Set in the muted colour under the heading." },
    hint: { type: ControlType.String, title: "Hint", defaultValue: "Scroll to pull focus", description: "Small text at the right of the heading. Hidden on phones." },
    linkLabel: { type: ControlType.String, title: "Link label", defaultValue: "View project", description: "Shown under the print in focus when its item has a link." },
    scale: { type: ControlType.Boolean, title: "Distance scale", defaultValue: true, description: "A lens distance scale under the heading. Its marker follows the scroll and each stop jumps to its photo." },
    shape: {
        type: ControlType.Enum,
        title: "Print shape",
        options: ["3:2 landscape", "4:5 portrait", "1:1 square", "16:9 wide"],
        optionTitles: ["3:2 landscape", "4:5 portrait", "1:1 square", "16:9 wide"],
        defaultValue: "3:2 landscape",
        description: "Every photo is cropped to this shape, so any photo works.",
    },
    travel: {
        type: ControlType.Number,
        title: "Scroll per photo",
        defaultValue: 55,
        min: 30,
        max: 160,
        step: 5,
        unit: "vh",
        description: "How much scrolling it takes to pull focus from one print to the next.",
    },
    topSpace: {
        type: ControlType.Number,
        title: "Top space",
        defaultValue: 84,
        min: 0,
        max: 200,
        step: 4,
        unit: "px",
        description: "Room above the prints for a fixed navigation bar. About three quarters of it on phones.",
    },
    imageWidth: {
        type: ControlType.String,
        title: "Image width",
        defaultValue: "62vw",
        description: "How wide the sharp print is on screen, for example 62vw. Used to load the right image size.",
    },
    preview: {
        type: ControlType.Number,
        title: "Canvas preview",
        defaultValue: 0,
        min: 0,
        max: 12,
        step: 1,
        displayStepper: true,
        description: "Which item is in focus on the canvas. The live site follows the scroll.",
    },
    fontFamily: {
        type: ControlType.String,
        title: "Font family",
        defaultValue: "Inter",
        description: "Use the family name of your text styles.",
    },
    ink: { type: ControlType.Color, title: "Text colour", defaultValue: "#ECE9E2" },
    muted: { type: ControlType.Color, title: "Muted colour", defaultValue: "#8E8B84" },
    line: { type: ControlType.Color, title: "Scale line", defaultValue: "rgba(236,233,226,0.38)" },
    accent: { type: ControlType.Color, title: "Marker colour", defaultValue: "#F0C445" },
    background: { type: ControlType.Color, title: "Background", defaultValue: "#0A0A0A", description: "Also the shade behind the heading." },
})

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.