Live values
Solid 2UnstableA lookbook with depth
Scroll the lookbook and each collection's photo drifts slower than its banner and its words faster, as if they sat at different depths.
Reduced motion: Nothing drifts: the photos and words scroll with their banners.
Why
A lookbook is for browsing, and depth makes it feel like a place: as you move, near things pass faster than far ones. Each banner's photo moves a little slower than the banner, and its words a little faster, so the photo reads as behind and the words in front.
Each banner asks createScroll for its own progress through the panel: with a target and Motion's offset, ["start end", "end start"], it's 0 as the banner's top comes in at the bottom and 1 as its bottom leaves at the top. createTransform maps that to a few dozen pixels of y for the photo and for the words, one each way.
A parallax is motion of your own, so reduced motion is yours to apply: Wardic snaps the animations it runs, and a live value follows what moves it. The demo's transforms give 0 under it.
The code
import { photoOf, srcsetOf } from "@wardic/example-storefront-shared/images"
import { type Collection, LOOKBOOK } from "@wardic/example-storefront-shared/stories"
import { createScroll, createTransform, wardic } from "@wardic/solid/unstable/v2"
import { For } from "solid-js"
import { useReducedMotion } from "~/lib/preferences.tsx"
export default function Parallax() {
let panel!: HTMLDivElement
return (
<div class="lookbook" ref={panel} tabindex="0" aria-label="The lookbook">
<For each={LOOKBOOK}>{(collection) => <Banner collection={collection} panel={() => panel} />}</For>
</div>
)
}
function Banner(props: { readonly collection: Collection; readonly panel: () => HTMLElement | undefined }) {
let banner!: HTMLElement
const reduced = useReducedMotion()
// How far the banner has come through the panel: 0 as its top comes in at the bottom, 1 as its
// bottom leaves at the top.
const { scrollYProgress } = createScroll({ container: props.panel, target: () => banner, offset: ["start end", "end start"] })
// The photo moves less than the banner, the words more: depth. Under reduced motion, neither.
const depth = (travel: number) => (progress: number) => {
if (reduced()) return 0
return (progress - 0.5) * travel
}
const photoY = createTransform(scrollYProgress, depth(90))
const wordsY = createTransform(scrollYProgress, depth(-70))
const photo = photoOf(props.collection.photo)
return (
<section class="lookbook-banner" ref={banner}>
<wardic.img class="lookbook-photo" style={{ y: photoY }} src={photo.large} srcset={srcsetOf(photo)} sizes="(max-width: 640px) 100vw, 720px" alt="" />
<wardic.div class="lookbook-words" style={{ y: wordsY }}>
<h3>{props.collection.title}</h3>
<p>{props.collection.blurb}</p>
</wardic.div>
</section>
)
}
"use client"
import { photoOf, srcsetOf } from "@wardic/example-storefront-shared/images"
import { type Collection, LOOKBOOK } from "@wardic/example-storefront-shared/stories"
import { motion, useReducedMotionConfig, useScroll, useTransform } from "motion/react"
import { type RefObject, useRef } from "react"
export default function Parallax() {
const panel = useRef<HTMLDivElement>(null)
return (
<div className="lookbook" ref={panel} tabIndex={0} aria-label="The lookbook">
{LOOKBOOK.map((collection) => (
<Banner key={collection.title} collection={collection} panel={panel} />
))}
</div>
)
}
function Banner({ collection, panel }: { readonly collection: Collection; readonly panel: RefObject<HTMLDivElement | null> }) {
const banner = useRef<HTMLElement>(null)
// The system's setting, or the header's switch (`MotionConfig`).
const reduced = useReducedMotionConfig()
// How far the banner has come through the panel: 0 as its top comes in at the bottom, 1 as its
// bottom leaves at the top.
const { scrollYProgress } = useScroll({ container: panel, target: banner, offset: ["start end", "end start"] })
// The photo moves less than the banner, the words more: depth. Under reduced motion, neither.
const depth = (travel: number) => (progress: number) => {
if (reduced) return 0
return (progress - 0.5) * travel
}
const photoY = useTransform(scrollYProgress, depth(90))
const wordsY = useTransform(scrollYProgress, depth(-70))
const photo = photoOf(collection.photo)
return (
<section className="lookbook-banner" ref={banner}>
<motion.img className="lookbook-photo" style={{ y: photoY }} src={photo.large} srcSet={srcsetOf(photo)} sizes="(max-width: 640px) 100vw, 720px" alt="" />
<motion.div className="lookbook-words" style={{ y: wordsY }}>
<h3>{collection.title}</h3>
<p>{collection.blurb}</p>
</motion.div>
</section>
)
}


