All-Access · 23 templates$1319.97$99/year — save 92%Get access
  • HomeHome
  • TemplatesTemplates17+
  • ComponentsComponents80+
  • PricingPricing
  • Need a Website?Need a Website?New
Unlock All AccessUnlock All Access

Our latest template collection

Ready-to-launch sites for real estate agents, service businesses, and local brands — built to rank, load fast, and convert.

Explore All TemplatesExplore All Templates

Orvaine - Luxury Equestrian & Horse Breeding Website Template

Orvaine is a premium website template for stud farms, sport horse breeders and equestrian estates: horse pages with pedigrees, events, films. Next.js, Astro, Sanity.

Download Template
Orvaine - Luxury Equestrian & Horse Breeding Website Template

Maressa - Luxury Yacht Charter & Sales Website Template

Maressa is a premium website template for yacht charter, brokerage and superyacht brands: 21 editable sections, full-screen films, Next.js and Astro versions. Sanity.

Download Template
Maressa - Luxury Yacht Charter & Sales Website Template

Krishi – Agriculture & Farm Website Template

Krishi is a premium website template for farms, agritech and food brands: 17 editable sections, cinematic GSAP motion and both Next.js and Astro versions. Built with Sanity.

Download Template
Krishi – Agriculture & Farm Website Template
ComponentsAnimationImage Trails
FreeReactTailwind CSSNext.js

Image Trails

A cursor trail that drops tilted photo cards which slice apart and fly off as you move. React, TypeScript and Tailwind CSS, no dependencies.

View codeView code
Full screen
ImageTrails.tsx
"use client";

// Cursor image trail: moving the pointer over the section drops tilted photo
// cards that pop in, then slice apart and fly off. Touch devices see a static stack.
// React + inline styles, no dependencies. Works with Tailwind v3 and v4.

import { useEffect, useRef } from "react";

type Props = {
  heading?: string;
  hint?: string;
  images?: string[];
};

const img = (id: string) => `https://images.unsplash.com/photo-${id}?w=480&q=75&auto=format&fit=crop`;

const DEFAULT_IMAGES = [
  img("1705860589032-a9c98cb3cfa8"),
  img("1622179662825-3ae0a26f1b26"),
  img("1604076913837-52ab5629fba9"),
  img("1496365597860-f19cab8ecdaf"),
  img("1504738165310-a8d01a22fbbd"),
  img("1565382038303-8c62e88d119a"),
  img("1555988776-c3f17e5cb789"),
  img("1592762723096-ab8e59e76f06"),
];

const MAX_CARDS = 12; // cards on screen at once
const SPAWN_DIST = 80; // px the pointer travels between cards
const CARD_W = 160;
const CARD_H = 200;
const LIFE_MS = 1200;
const CHOP_DELAY = 400; // when a card starts slicing apart
const PIECES = 6;
const ACCENTS = ["#FF3D00", "#FFB300", "#E53935", "#1565C0", "#FFFFFF"];

const rand = (min: number, max: number) => Math.random() * (max - min) + min;

type Card = { el: HTMLDivElement; timers: number[] };

function spawn(stage: HTMLElement, x: number, y: number, src: string, index: number): Card {
  const tilt = rand(-18, 18);
  const scale = rand(0.88, 1.08);
  const el = document.createElement("div");
  el.style.cssText = `position:absolute;left:${x - CARD_W / 2}px;top:${y - CARD_H / 2}px;width:${CARD_W}px;height:${CARD_H}px;pointer-events:none;overflow:hidden;border-radius:3px;transform:rotate(${tilt}deg) scale(0);transform-origin:center bottom;transition:transform .25s cubic-bezier(.34,1.56,.64,1),opacity .25s ease;`;
  el.innerHTML = `<div style="position:absolute;inset:0;background:url(${src}) center/cover no-repeat"></div><div style="position:absolute;inset:0;background:${ACCENTS[index % ACCENTS.length]};mix-blend-mode:multiply;opacity:.35"></div>`;
  stage.appendChild(el);
  requestAnimationFrame(() => {
    el.style.transform = `rotate(${tilt}deg) scale(${scale})`;
  });

  const chop = window.setTimeout(() => {
    // Rebuild the card as vertical slices of the same photo, then send each flying
    el.innerHTML = "";
    for (let i = 0; i < PIECES; i++) {
      const piece = document.createElement("div");
      piece.style.cssText = `position:absolute;top:0;height:100%;left:${(i * 100) / PIECES}%;width:${100 / PIECES}%;background:url(${src}) ${(-i * CARD_W) / PIECES}px 0/${CARD_W}px ${CARD_H}px no-repeat;transition:transform ${280 + i * 30}ms cubic-bezier(.4,0,1,1) ${i * 28}ms,opacity 200ms ease ${i * 28}ms;`;
      el.appendChild(piece);
      requestAnimationFrame(() =>
        requestAnimationFrame(() => {
          piece.style.transform = `translate(${rand(-120, 120)}px,${rand(-160, -60)}px) rotate(${rand(-45, 45)}deg)`;
          piece.style.opacity = "0";
        }),
      );
    }
    el.style.transition = `opacity ${LIFE_MS - CHOP_DELAY}ms ease`;
    el.style.opacity = "0";
  }, CHOP_DELAY);
  const kill = window.setTimeout(() => el.remove(), LIFE_MS + 100);

  return { el, timers: [chop, kill] };
}

export default function ImageTrails({
  heading = "Move your cursor",
  hint = "Every 80px leaves a photo behind",
  images = DEFAULT_IMAGES,
}: Props) {
  const stageRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const stage = stageRef.current;
    if (!stage || window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
    let cards: Card[] = [];
    let last = { x: -9999, y: -9999 };
    let count = 0;

    const onMove = (e: PointerEvent) => {
      if (e.pointerType === "touch") return;
      const rect = stage.getBoundingClientRect();
      const x = e.clientX - rect.left;
      const y = e.clientY - rect.top;
      if (Math.hypot(x - last.x, y - last.y) < SPAWN_DIST) return;
      last = { x, y };

      cards = cards.filter((c) => c.el.isConnected);
      if (cards.length >= MAX_CARDS) {
        const old = cards.shift()!;
        old.timers.forEach(clearTimeout);
        old.el.remove();
      }
      cards.push(spawn(stage, x, y, images[count % images.length], count));
      count++;
    };

    stage.addEventListener("pointermove", onMove);
    return () => {
      stage.removeEventListener("pointermove", onMove);
      cards.forEach((c) => {
        c.timers.forEach(clearTimeout);
        c.el.remove();
      });
    };
  }, [images]);

  return (
    <section ref={stageRef} className="relative flex min-h-[600px] cursor-crosshair items-center justify-center overflow-hidden bg-neutral-950 px-6 font-sans antialiased">
      <div className="pointer-events-none relative z-10 text-center mix-blend-difference">
        <h2 className="text-[clamp(48px,9vw,140px)] font-black uppercase leading-[0.9] tracking-[-0.04em] text-white">{heading}</h2>
        <p className="mt-6 font-mono text-xs uppercase tracking-[0.2em] text-white/60">{hint}</p>
      </div>
    </section>
  );
}

How to use it

  1. Make sure your Next.js or React project has Tailwind CSS set up.
  2. Create components/ImageTrails.tsx and paste the code.
  3. Import it into a page and replace the placeholder text and images with your own.
Need the whole website?

Skip the assembly

Our Next.js & Astro templates ship every page already built, with Sanity CMS, SEO and fast load times included.

Browse templates Browse templates

More free components

All animation →
Count-Up Stats

Count-Up Stats

Expanding Image Panels

Expanding Image Panels

Fading Quote Testimonials

Fading Quote Testimonials

Launch a full site instead

Orvaine - Luxury Equestrian & Horse Breeding Website TemplateNext.js + Astro
All-Access PREMIUM NEXTJS ASTRO
Orvaine - Luxury Equestrian & Horse Breeding Website Template

Orvaine is a premium website template for stud farms, sport horse breeders and equestrian estates: h...

Preview
Maressa - Luxury Yacht Charter & Sales Website TemplateNext.js + Astro
All-Access NEXTJS ASTRO PREMIUM
Maressa - Luxury Yacht Charter & Sales Website Template

Maressa is a premium website template for yacht charter, brokerage and superyacht brands: 21 editabl...

Preview
Krishi – Agriculture & Farm Website TemplateNext.js + Astro
All-Access PREMIUM NEXTJS ASTRO
Krishi – Agriculture & Farm Website Template

Krishi is a premium website template for farms, agritech and food brands: 17 editable sections, cine...

Preview

Ready for a faster site that's easier to edit?

Book a free 30-minute call. We'll review your current site and send a fixed-price plan. No pressure, no pitch.

Book Free Call Book Free Call No commitment required

Fast Next.js + Sanity websites, built for you or ready to launch with our templates.

Book a Free Call Book a Free Call
Services
  • Web Design
  • UI/UX Design
  • Framer Dev
  • Webflow Dev
  • Custom Dev
  • Sanity CMS
Resources
  • Templates
  • Pricing
  • License
  • Components
  • Case Studies
Company
  • About Us
  • Contact
  • Privacy Policy
© 2026 rayso.studio — All rights reservedDesigned & built in-house Remote-first agency
Let's TalkLet's Talk