Image Ticker

v1.0.1
FreeShowcase & Media

A gorgeous, cinematic dual-ticker displaying rows of images rotating in opposite directions with built-in lightbox functionality.

Image Ticker

Installation

Step 1. Initialize ProjectOne-time setup

Run once in your project root to configure paths and dependencies:

npx vectorvesper init
Step 2. Add ComponentInstalls source & assets

Downloads and registers the component in your project:

npx vectorvesper add image-ticker
Step 3. Required DependenciesAuto-installed with CLI

If installing manually or managing your package lock:

npm install framer-motion
Packages:framer-motion

Usage

Import

import ImageTicker from "@/components/vv/image-ticker/ImageTicker";

Usage

<ImageTicker />
This component respects prefers-reduced-motion and provides a graceful fallback.

Props

Two rows of images sliding past each other at opposing angles, with a headline set across them. Cards lift and brighten under the cursor, and clicking one opens it full-size.

<ImageTicker />

The finished section — both rows, the headline, the atmospherics and the lightbox.

PropTypeDefaultDescription
topImages*string[]Images for the upper row.
bottomImages*string[]Images for the lower row, which travels the other way.
topRotationnumber35Angle of the upper row in degrees.
bottomRotationnumber-35Angle of the lower row. Negative by default so the two rows cross.
backgroundColorstring"#030303"Colour behind the section.
heightstring"100vh"Height of the section, as any CSS length.
titlestringHeadline drawn over the rows. Omit it and no headline renders.
titleFontFamilystring"'Bebas Neue', sans-serif"Face for the headline. The default names a font the component does not load — point this at one of yours, or load Bebas Neue.
titleFontSizestring"clamp(2rem, 4vw, 6rem)"Headline size. Fluid by default, so it scales with the viewport.
titleColorstring"rgba(255, 255, 255, 0.95)"Headline colour.
titlePosition"left" | "right" | "center""right"Which side of the section the headline sits on.
titleOffsetstring"8vw"How far in from that edge, as any CSS length.
showAmbientGlowbooleantrueA soft wash of colour behind the rows.
ambientGlowColorstring"rgba(127,140,255,0.06)"Colour of that wash. Very low alpha by design.
showNoiseGrainbooleantrueFine grain over the whole section.
showVignettebooleantrueDarkens the edges of the section.
enableLightboxbooleantrueWhether clicking a card opens it full-size.
onImageClick(src: string) => voidCalled with the clicked image's URL. Use it to route somewhere instead of opening the built-in lightbox.
tickerPropsPartial<SmoothTickerProps>{}Passed through to both rows — card size, speed, hover treatment. `images`, `direction` and `onCardClick` are managed for you and cannot be set here.
lightboxPropsPartial<LightboxOverlayProps>{}Passed through to the lightbox. `src` and `onClose` are managed for you.
classNamestringApplied to the section wrapper.

<SmoothTicker />

One row on its own, if you want a single strip rather than the crossed pair.

PropTypeDefaultDescription
images*string[]Images in the row.
direction1 | -11Which way the row travels.
speednumber0.05How fast it travels.
cardWidthnumber170Width of one card in pixels.
cardHeightnumber200Height of one card.
gapnumber20Space between cards.
cardBorderRadiusnumber14Corner radius of a card.
cardBackgroundstring"#0a0a0e"Colour behind a card's image.
containerHeightnumber230Height of the row's track.
containerBorderRadiusnumber30Corner radius of that track.
containerBackgroundstring"#060608"Colour of the track.
pauseOnHoverbooleantrueStops the row while the cursor is over it.
showHoverGlowbooleantrueLights a card under the cursor.
showShineSweepbooleantrueRuns a highlight across a hovered card.
showGradientBorderbooleantrueDraws a gradient edge on a hovered card.
onCardClick(src: string) => voidCalled with the clicked image's URL.
classNamestringApplied to the row.

<LightboxOverlay />

The full-size viewer, usable on its own. Closes on any click.

PropTypeDefaultDescription
src*string | nullImage to show. Pass null to close it.
onClose*() => voidCalled when it should close.
imageWidthstring"420px"Width of the shown image.
imageHeightstring"540px"Height of the shown image.
borderRadiusstring"24px"Corner radius of the image.
overlayColorstring"rgba(3,3,3,0.8)"Colour of the backdrop.
backdropBlurstring"24px"How far the backdrop blurs what is behind it.
showCloseHintbooleantrueShows the line telling people how to dismiss it.
closeHintTextstring"Click anywhere to close"That line's text.
showAmbientGlowbooleantrueA soft wash behind the image.
classNamestringApplied to the overlay.

Examples

The full section
import ImageTicker from "@/components/vv/image-ticker/ImageTicker";

<ImageTicker
  topImages={rowA}
  bottomImages={rowB}
  title="SELECTED WORK"
/>
Bigger cards, slower travel
<ImageTicker
  topImages={rowA}
  bottomImages={rowB}
  title="ARCHIVE"
  titlePosition="left"
  tickerProps={{ cardWidth: 260, cardHeight: 320, speed: 0.02, gap: 32 }}
/>
Route on click instead of opening the lightbox
<ImageTicker
  topImages={rowA}
  bottomImages={rowB}
  enableLightbox={false}
  onImageClick={(src) => router.push(`/work/${slugFor(src)}`)}
/>
The rows run continuously with no reduced-motion guard. If that matters, set tickerProps={{ speed: 0 }} when the preference is set — the layout stays, the travel stops.
The default headline face is Bebas Neue, which the component does not load for you. Either load it or set titleFontFamily to one of your own.
Both rows duplicate their images to loop seamlessly, so each list is drawn more than once. Ship the images near the size the cards show them at.

Source

"use client";

/**
 * Image Ticker — Vector Vesper
 * https://vectorvesper.dev/components
 *
 * Copyright (c) 2026 Vector Vesper
 * Released under the MIT License. This notice must be retained in copies and
 * substantial portions of the file. https://vectorvesper.dev/license
 */
import { motion } from "framer-motion";
import React, { useState } from "react";
import LightboxOverlay from "./LightboxOverlay";
import SmoothTicker from "./SmoothTicker";

import type { LightboxOverlayProps } from "./LightboxOverlay";
import type { SmoothTickerProps } from "./SmoothTicker";

export type ImageTickerProps = {
  topImages: string[];
  bottomImages: string[];
  topRotation?: number;
  bottomRotation?: number;
  backgroundColor?: string;
  height?: string;
  title?: string;
  titleFontFamily?: string;
  titleFontSize?: string;
  titleColor?: string;
  titlePosition?: "left" | "right" | "center";
  titleOffset?: string;
  showAmbientGlow?: boolean;
  ambientGlowColor?: string;
  showNoiseGrain?: boolean;
  showVignette?: boolean;
  enableLightbox?: boolean;
  onImageClick?: (src: string) => void;
  tickerProps?: Partial<
    Omit<SmoothTickerProps, "images" | "direction" | "onCardClick">
  >;
  lightboxProps?: Partial<Omit<LightboxOverlayProps, "src" | "onClose">>;
  className?: string;
};

const ImageTicker = ({
  topImages,
  bottomImages,
  topRotation = 35,
  bottomRotation = -35,
  backgroundColor = "#030303",
View on GitHubReport an issue