dqnamoexperiments / iridescent-foil

Iridescent Foil

Holographic foil built from layered CSS gradients. Scroll the page and move your pointer to shift the iridescent colour and specular glare across the sticker.

"use client";import {  type ComponentPropsWithoutRef,  type CSSProperties,  useEffect,  useRef,} from "react";import { cn } from "@/helpers/classname-helper";import styles from "./IridescentFoil.module.css";type ScrollProgressMode = "element" | "document";type IridescentFoilCustomProperty =  | "--foil-shift"  | "--foil-y-shift"  | "--glare-x"  | "--glare-y"  | "--pointer-x"  | "--pointer-y"  | "--shine-angle"  | "--shine-opacity";type IridescentFoilStyle = CSSProperties &  Record<IridescentFoilCustomProperty, string>;type IridescentFoilProps = ComponentPropsWithoutRef<"div"> & {  scrollProgressMode?: ScrollProgressMode;};const initialStyle: IridescentFoilStyle = {  "--foil-shift": "0%",  "--foil-y-shift": "0%",  "--glare-x": "50%",  "--glare-y": "50%",  "--pointer-x": "50%",  "--pointer-y": "50%",  "--shine-angle": "135deg",  "--shine-opacity": "0.62",};function clamp(value: number, min: number, max: number) {  return Math.min(Math.max(value, min), max);}function toPercent(value: number) {  return `${Number((value * 100).toFixed(3))}%`;}function toDegrees(value: number) {  return `${Number(value.toFixed(3))}deg`;}function toUnitless(value: number) {  return `${Number(value.toFixed(3))}`;}function getElementScrollProgress(element: HTMLElement) {  const rect = element.getBoundingClientRect();  const visibleScrollRange = window.innerHeight + rect.height;  return visibleScrollRange > 0    ? clamp((window.innerHeight - rect.top) / visibleScrollRange, 0, 1)    : 0;}function getDocumentScrollProgress() {  const scrollingElement =    document.scrollingElement ?? document.documentElement;  const scrollRange = scrollingElement.scrollHeight - window.innerHeight;  return scrollRange > 0    ? clamp(scrollingElement.scrollTop / scrollRange, 0, 1)    : 0;}function getElementPointer(event: PointerEvent, element: HTMLElement) {  const rect = element.getBoundingClientRect();  return {    x:      rect.width > 0        ? clamp((event.clientX - rect.left) / rect.width, 0.08, 0.92)        : 0.5,    y:      rect.height > 0        ? clamp((event.clientY - rect.top) / rect.height, 0.08, 0.92)        : 0.5,  };}function setFoilProperty(  style: CSSStyleDeclaration,  property: IridescentFoilCustomProperty,  value: string,) {  style.setProperty(property, value);}export function IridescentFoil({  children,  className,  scrollProgressMode = "element",  style,  ...props}: IridescentFoilProps) {  const foilRef = useRef<HTMLDivElement>(null);  const frameRef = useRef<number | null>(null);  const pointerRef = useRef({ x: 0.5, y: 0.5 });  useEffect(() => {    const foil = foilRef.current;    if (!foil) {      return;    }    const applyEffect = () => {      const { x, y } = pointerRef.current;      const scrollProgress =        scrollProgressMode === "document"          ? getDocumentScrollProgress()          : getElementScrollProgress(foil);      const foilStyle = foil.style;      setFoilProperty(        foilStyle,        "--foil-shift",        toPercent(scrollProgress * 0.82 + (x - 0.5) * 0.18),      );      setFoilProperty(        foilStyle,        "--foil-y-shift",        toPercent(scrollProgress * 0.28 + (y - 0.5) * 0.12),      );      setFoilProperty(foilStyle, "--glare-x", toPercent(x));      setFoilProperty(foilStyle, "--glare-y", toPercent(y));      setFoilProperty(foilStyle, "--pointer-x", toPercent(x));      setFoilProperty(foilStyle, "--pointer-y", toPercent(y));      setFoilProperty(        foilStyle,        "--shine-angle",        toDegrees(105 + scrollProgress * 80 + (x - 0.5) * 28),      );      setFoilProperty(        foilStyle,        "--shine-opacity",        toUnitless(0.56 + Math.abs(x - 0.5) * 0.24 + scrollProgress * 0.12),      );      frameRef.current = null;    };    const scheduleEffect = () => {      if (frameRef.current !== null) {        return;      }      frameRef.current = window.requestAnimationFrame(applyEffect);    };    const handlePointerMove = (event: PointerEvent) => {      pointerRef.current = getElementPointer(event, foil);      scheduleEffect();    };    const handleScroll = () => {      scheduleEffect();    };    window.addEventListener("pointermove", handlePointerMove, {      passive: true,    });    window.addEventListener("scroll", handleScroll, { passive: true });    window.addEventListener("resize", scheduleEffect, { passive: true });    scheduleEffect();    return () => {      window.removeEventListener("pointermove", handlePointerMove);      window.removeEventListener("scroll", handleScroll);      window.removeEventListener("resize", scheduleEffect);      if (frameRef.current !== null) {        window.cancelAnimationFrame(frameRef.current);      }    };  }, [scrollProgressMode]);  return (    <div      {...props}      ref={foilRef}      className={cn(styles.surface, className)}      style={{ ...initialStyle, ...style }}    >      <span aria-hidden className={styles.foil} />      <span aria-hidden className={styles.film} />      <span aria-hidden className={styles.pearl} />      <div className={styles.content}>{children}</div>      <span aria-hidden className={styles.shine} />      <span aria-hidden className={styles.glare} />    </div>  );}

Want to work with me?

I run a lil design engineering studio in London where we do fractional design engineering for startups.

Jump on a call
THEINTERFACECOMPANYOF LONDON