Sei un senior frontend engineer. Stai lavorando su un sito Next.js 16 + React 19 + Tailwind v4 in italiano, look chanhdai-inspired: colonna stretta 672px, Geist Sans + Geist Mono, hairline 1px, divisori a stripe diagonale, palette zinc. Token CSS disponibili: --bg, --bg-alt, --fg, --fg-muted, --fg-soft, --border, --border-strong, --accent. Usa SEMPRE queste variabili tramite le utility tailwind generate (bg-bg, text-fg-muted, border-border, ecc.). Helper "cn" da "@/lib/utils". Niente librerie UI extra: solo lucide-react e tailwind-merge. Genera un client component <ParallaxLane>: wrapper che applica un piccolo translateY scroll-tied al figlio. Props: - multiplier?: number (default 0.04) — scrollY * multiplier = offset raw. Negativo = direzione opposta allo scroll. - maxOffsetPx?: number (default 110) — asintoto del tanh. - slackPct?: number (default 20) — slack verticale sopra/sotto, in percentuale dell'altezza outer. - children: ReactNode - className?: string Comportamento: - "use client". - useEffect: registra scroll listener passive con rAF throttle (un solo frame in volo). Apply iniziale per cogliere reload con scroll già non-zero. - Skip se matchMedia "(prefers-reduced-motion: reduce)" matches. - Cleanup: removeEventListener + cancelAnimationFrame. - offset = Math.tanh(scrollY * multiplier / maxOffsetPx) * maxOffsetPx (asintoto, no snap). - Scrivi il transform direttamente sull'elemento (innerRef.style.transform = "translate3d(0, Ypx, 0)") — niente CSS variable per evitare recalc inheritati. Layout: container outer "relative h-full overflow-hidden". Inner absolute inset-x-0 con top: -slackPct% bottom: -slackPct% (slack equa sopra/sotto), will-change-transform. Il children dentro l'inner. Constraints: zero dipendenze JS extra, transform-only, accessibile (decorativo, aria-hidden gestito dal parent), funziona dentro <MarqueeVertical>. Output: file completo .tsx (con "use client").
Component
LiveNewParallax Lane
Wrapper client component: ascolta lo scroll passive, applica un translate3d Y al figlio = tanh(scrollY × multiplier / max) × max. rAF-throttled, asintotico (no snap ai bordi), slack 22% sopra/sotto così non si vedono mai bordi vuoti. Pensato per parallax molto leggera sui marquee delle side columns.
Anteprima01
Note — Curva tanh asintotica: lineare per scrollY piccoli, tende ad ±maxOffsetPx senza mai raggiungerlo. Niente snap al boundary. Il transform viene scritto direttamente sull'elemento (no CSS variable) così i siblings non rifanno style-recalc. prefers-reduced-motion: skip totale.
Prompt LLM02
Incolla in Claude o ChatGPT per generare la tua variante. Include il contesto del brand, i token e i vincoli del progetto.
Codice03
Sorgente live di src/components/parallax-lane.tsx. Letto a request-time dal repo — sempre allineato al sito.
"use client";
import { useEffect, useRef, type ReactNode } from "react";
import { cn } from "@/lib/utils";
import { prefersReducedMotion } from "./use-reduced-motion";
export type ParallaxLaneProps = {
/**
* Multiplier applied to scrollY (px) to produce the lane's translateY.
* Use small values (~0.02–0.07). Negative values move opposite to scroll.
*/
multiplier?: number;
/**
* Maximum absolute translation in pixels — clamped so we never exceed
* the slack space wrapping the lane. Default 110.
*/
maxOffsetPx?: number;
/**
* Vertical slack as a percentage of the lane height. The inner translates
* within this slack — set on both sides of the wrapper as `inset-y` extra
* space. Default 20 (so inner = 140% of outer height).
*/
slackPct?: number;
children: ReactNode;
className?: string;
};
/**
* Wraps a marquee lane with a small scroll-tied parallax translation.
*
* Motion notes (Emil playbook):
* - Scroll is rAF-throttled with a single requestAnimationFrame cycle —
* no work happens between frames if the user isn't scrolling.
* - We write `transform` directly to the element (not a CSS variable) so
* siblings don't recompute styles. Only this node's compositor layer
* moves — true GPU work.
* - The lane has slack (default 20%) above and below it, so the parallax
* translation never exposes empty space at the gutter edges.
* - The translation uses a tanh asymptote, not a clamp: linear-ish at
* small scrollY, smoothly approaching ±maxOffsetPx as scrollY grows.
* No snap at the boundary, no "stuck" feel on very long pages.
* - `prefers-reduced-motion` users get no parallax — the marquee itself
* is also collapsed by the global rule.
*/
export function ParallaxLane({
multiplier = 0.04,
maxOffsetPx = 110,
slackPct = 20,
children,
className,
}: ParallaxLaneProps) {
const innerRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (typeof window === "undefined") return;
if (prefersReducedMotion()) {
return;
}
let frame = 0;
let scheduled = false;
const apply = () => {
scheduled = false;
const el = innerRef.current;
if (!el) return;
// tanh asymptote: linear-ish at small scrollY, smoothly approaches
// ±maxOffsetPx as scrollY grows. No snap at the boundary.
const raw = window.scrollY * multiplier;
const y = Math.tanh(raw / maxOffsetPx) * maxOffsetPx;
el.style.transform = `translate3d(0, ${y.toFixed(2)}px, 0)`;
};
const onScroll = () => {
if (scheduled) return;
scheduled = true;
frame = requestAnimationFrame(apply);
};
// Apply once on mount in case the page loaded already-scrolled.
apply();
window.addEventListener("scroll", onScroll, { passive: true });
return () => {
window.removeEventListener("scroll", onScroll);
if (frame) cancelAnimationFrame(frame);
};
}, [multiplier, maxOffsetPx]);
// The outer crops slack; the inner is taller than outer by 2× slackPct
// and centred (inset-y: -slackPct%).
const insetY = `-${slackPct}%`;
return (
<div className={cn("relative h-full overflow-hidden", className)}>
<div
ref={innerRef}
className="absolute inset-x-0 will-change-transform"
style={{
top: insetY,
bottom: insetY,
transform: "translate3d(0, 0, 0)",
}}
>
{children}
</div>
</div>
);
}
Uso tipico04
<ParallaxLane multiplier={0.04} slackPct={22}>
<MarqueeVertical ...>{children}</MarqueeVertical>
</ParallaxLane>Dipendenze05
- tailwind-merge
- clsx
- @/lib/utils#cn
Ti è servito? Dimmelo, oppure proponi il prossimo componente.
