awesome-gallery-infinity
v0.1.6
Published
A stunning infinite-scroll 3D perspective gallery React component with Panorama and Cinema view modes, drag-to-scroll, hover pause, and a built-in card detail modal.
Maintainers
Readme
awesome-gallery-infinity
A stunning infinite-scroll 3D perspective gallery React component. Cards flow endlessly in tilted multi-column columns with a parallax speed differential between columns, two switchable view modes (Panorama and Cinema), drag-to-scroll with inertia, hover-to-pause, mouse wheel and arrow key control, and a built-in card detail modal — zero external dependencies beyond React.
Features
- Infinite bidirectional scroll — seamless triple-track loop; scroll up or down forever
- Two view modes — Panorama (strong tilt, tight columns, asymmetric stagger) and Cinema (shallow tilt, wide columns, cinematic feel)
- Parallax column speeds — centre column fastest, outer columns progressively slower for a 3D depth illusion
- Drag to scroll — pointer drag with inertia throw; distinct click vs drag detection
- Mouse wheel + arrow keys — wheel nudges speed up/down;
↑/↓keys adjust speed - Hover to pause — hovering any card freezes all columns; leaving resumes
- Built-in card detail modal — click a card to open an animated modal with title, subtitle, tag, and CTA buttons
onCardClickoverride — skip the modal entirely and handle navigation yourself- Vignette edges — top, bottom, and side fades give a clean infinite feel
- ESM + CJS dual build — works in Vite, Next.js, CRA, and any modern bundler
- Full TypeScript types included
Installation
npm install awesome-gallery-infinity
# or
yarn add awesome-gallery-infinity
# or
pnpm add awesome-gallery-infinityPeer dependencies — React ≥ 17 and ReactDOM ≥ 17 must already be installed.
Importing Styles
The component uses CSS keyframe animations (fadeInCard, slideUpModal) and CSS custom properties (--font-display, --font-ui). Import the stylesheet once at your app root:
import 'awesome-gallery-infinity/style.css';For the full typographic effect, add these Google Fonts to your index.html <head>:
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Syne:wght@600;700&display=swap"
rel="stylesheet"
/>Falls back to
sans-serifwithout the fonts — the component still renders correctly.
Quick Start
import { AwesomeGalleryInfinity } from 'awesome-gallery-infinity';
import 'awesome-gallery-infinity/style.css';
var cards = [
{ id: 1, title: 'Aurora', subtitle: 'Northern Lights', tag: 'Nature', accent: '#00f5c8', bgFrom: '#020d1a', bgTo: '#041a12', aspectRatio: 1.45 },
{ id: 2, title: 'Nebula', subtitle: 'Deep Space Cloud', tag: 'Space', accent: '#a855f7', bgFrom: '#0d0520', bgTo: '#1a0535', aspectRatio: 1.25 },
{ id: 3, title: 'Coral', subtitle: 'Reef Ecosystem', tag: 'Ocean', accent: '#f97316', bgFrom: '#1a0800', bgTo: '#200d00', aspectRatio: 1.6 },
{ id: 4, title: 'Glacier', subtitle: 'Ancient Ice', tag: 'Arctic', accent: '#38bdf8', bgFrom: '#020d1a', bgTo: '#041428', aspectRatio: 1.35 },
{ id: 5, title: 'Volcano', subtitle: 'Tectonic Power', tag: 'Earth', accent: '#ef4444', bgFrom: '#1a0404', bgTo: '#200808', aspectRatio: 1.5 },
{ id: 6, title: 'Canyon', subtitle: 'Carved by Time', tag: 'Desert', accent: '#f59e0b', bgFrom: '#1a0e00', bgTo: '#201200', aspectRatio: 1.4 },
{ id: 7, title: 'Biolume', subtitle: 'Living Light', tag: 'Deep Sea', accent: '#22d3ee', bgFrom: '#020d18', bgTo: '#031520', aspectRatio: 1.55 },
{ id: 8, title: 'Summit', subtitle: 'Above the Clouds', tag: 'Alpine', accent: '#e2e8f0', bgFrom: '#0d0d14', bgTo: '#141420', aspectRatio: 1.3 },
{ id: 9, title: 'Savanna', subtitle: 'Open Plains', tag: 'Wildlife', accent: '#84cc16', bgFrom: '#0a1200', bgTo: '#101800', aspectRatio: 1.45 },
{ id: 10, title: 'Abyss', subtitle: 'Ocean Floor', tag: 'Mystery', accent: '#6366f1', bgFrom: '#060318', bgTo: '#0a0520', aspectRatio: 1.5 },
];
export default function App() {
return (
<div style={{ width: '100vw', height: '100vh' }}>
<AwesomeGalleryInfinity cards={cards} />
</div>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| cards | CardData[] | required | Array of card objects. See CardData below. Provide at least 5 cards for best visual results. |
| columns | number | 5 | Number of columns. The view configs are tuned for 5; fewer columns will still work. |
| defaultMode | 'Panorama' \| 'Cinema' | 'Panorama' | Which view mode to start in. Users can switch at runtime via the built-in nav bar. |
| onCardClick | (card: CardData) => void | — | If provided, fires on card click and skips the built-in modal entirely. Use for custom routing or side panels. |
CardData object
| Field | Type | Required | Description |
|---|---|---|---|
| id | number | ✅ | Unique identifier. Used as React key. |
| title | string | ✅ | Main heading on the card. First two characters are used as the large placeholder initial in the modal. |
| subtitle | string | ✅ | Secondary line shown below the title on the card and in the modal. |
| tag | string | ✅ | Category badge with a glowing dot, shown top-left of the card and in the modal header. |
| accent | string | ✅ | Hex neon accent colour (e.g. '#00f5c8'). Drives glow, borders, grid lines, and the modal CTA button. |
| bgFrom | string | ✅ | Gradient start colour (hex). Background of the card and modal visual area. |
| bgTo | string | ✅ | Gradient end colour (hex). |
| aspectRatio | number | ✅ | Height-to-width ratio of the card (e.g. 1.4 = portrait, 1.0 = square). Mix values across cards for a varied layout. |
Examples
Example 1 — Minimal, default Panorama mode
import { AwesomeGalleryInfinity } from 'awesome-gallery-infinity';
import 'awesome-gallery-infinity/style.css';
var cards = [
{ id: 1, title: 'Hydrogen', subtitle: 'Element #1', tag: 'Element', accent: '#38bdf8', bgFrom: '#020d1a', bgTo: '#031828', aspectRatio: 1.4 },
{ id: 2, title: 'Carbon', subtitle: 'Element #6', tag: 'Element', accent: '#4ade80', bgFrom: '#031a08', bgTo: '#042010', aspectRatio: 1.6 },
{ id: 3, title: 'Oxygen', subtitle: 'Element #8', tag: 'Element', accent: '#60a5fa', bgFrom: '#020d1a', bgTo: '#031424', aspectRatio: 1.3 },
{ id: 4, title: 'Iron', subtitle: 'Element #26', tag: 'Element', accent: '#f87171', bgFrom: '#1a0404', bgTo: '#200606', aspectRatio: 1.5 },
{ id: 5, title: 'Gold', subtitle: 'Element #79', tag: 'Element', accent: '#fbbf24', bgFrom: '#1a1000', bgTo: '#201400', aspectRatio: 1.35 },
{ id: 6, title: 'Neon', subtitle: 'Element #10', tag: 'Element', accent: '#f472b6', bgFrom: '#1a0418', bgTo: '#200520', aspectRatio: 1.55 },
{ id: 7, title: 'Silicon', subtitle: 'Element #14', tag: 'Element', accent: '#818cf8', bgFrom: '#06021a', bgTo: '#0a0320', aspectRatio: 1.4 },
{ id: 8, title: 'Xenon', subtitle: 'Element #54', tag: 'Element', accent: '#a78bfa', bgFrom: '#0a0420', bgTo: '#0d0528', aspectRatio: 1.5 },
{ id: 9, title: 'Cobalt', subtitle: 'Element #27', tag: 'Element', accent: '#2dd4bf', bgFrom: '#021814', bgTo: '#032018', aspectRatio: 1.3 },
{ id: 10, title: 'Uranium', subtitle: 'Element #92', tag: 'Element', accent: '#a3e635', bgFrom: '#081a02', bgTo: '#0a2003', aspectRatio: 1.6 },
];
export default function ElementsDemo() {
return (
<div style={{ width: '100vw', height: '100vh' }}>
<AwesomeGalleryInfinity cards={cards} />
</div>
);
}Example 2 — Cinema mode with onCardClick routing
Starts in Cinema mode and bypasses the built-in modal, sending the user to a custom route instead.
import { useCallback } from 'react';
import { AwesomeGalleryInfinity } from 'awesome-gallery-infinity';
import 'awesome-gallery-infinity/style.css';
var products = [
{ id: 1, title: 'Phantom', subtitle: 'Pro Wireless Headset', tag: 'Audio', accent: '#00f5c8', bgFrom: '#020d1a', bgTo: '#041a12', aspectRatio: 1.5 },
{ id: 2, title: 'Vortex', subtitle: '4K Gaming Monitor', tag: 'Display', accent: '#a855f7', bgFrom: '#0d0520', bgTo: '#1a0535', aspectRatio: 1.3 },
{ id: 3, title: 'Apex', subtitle: 'Mechanical Keyboard', tag: 'Input', accent: '#f97316', bgFrom: '#1a0800', bgTo: '#200d00', aspectRatio: 1.6 },
{ id: 4, title: 'Stealth', subtitle: 'Laser Gaming Mouse', tag: 'Input', accent: '#38bdf8', bgFrom: '#020d1a', bgTo: '#041428', aspectRatio: 1.4 },
{ id: 5, title: 'Nova', subtitle: 'RGB SSD 2TB', tag: 'Storage', accent: '#ef4444', bgFrom: '#1a0404', bgTo: '#200808', aspectRatio: 1.35 },
{ id: 6, title: 'Pulse', subtitle: 'Gaming Controller', tag: 'Control', accent: '#f59e0b', bgFrom: '#1a0e00', bgTo: '#201200', aspectRatio: 1.55 },
{ id: 7, title: 'Echo', subtitle: '7.1 Surround Speaker', tag: 'Audio', accent: '#22d3ee', bgFrom: '#020d18', bgTo: '#031520', aspectRatio: 1.45 },
{ id: 8, title: 'Orbit', subtitle: 'VR Headset Gen 3', tag: 'Immersive',accent: '#e879f9', bgFrom: '#1a041a', bgTo: '#200520', aspectRatio: 1.5 },
];
export default function ProductGallery() {
var handleCardClick = useCallback(function(card) {
// Navigate to a product page — replace with your router
window.location.href = '/products/' + card.id;
}, []);
return (
<div style={{ width: '100vw', height: '100vh' }}>
<AwesomeGalleryInfinity
cards={products}
defaultMode="Cinema"
onCardClick={handleCardClick}
/>
</div>
);
}Example 3 — Custom column count with onCardClick state panel
Uses 4 columns and renders a custom side panel instead of the built-in modal.
import { useState } from 'react';
import { AwesomeGalleryInfinity } from 'awesome-gallery-infinity';
import 'awesome-gallery-infinity/style.css';
var artworks = [
{ id: 1, title: 'Rift', subtitle: 'Digital Oil, 2024', tag: 'Abstract', accent: '#f472b6', bgFrom: '#1a0414', bgTo: '#20051a', aspectRatio: 1.6 },
{ id: 2, title: 'Zenith', subtitle: 'Generative, 2024', tag: 'Generative', accent: '#818cf8', bgFrom: '#06021a', bgTo: '#0a0325', aspectRatio: 1.4 },
{ id: 3, title: 'Hollow', subtitle: 'Mixed Media, 2023', tag: 'Mixed', accent: '#34d399', bgFrom: '#021a0e', bgTo: '#032012', aspectRatio: 1.5 },
{ id: 4, title: 'Flare', subtitle: 'Photography, 2024', tag: 'Photo', accent: '#fb923c', bgFrom: '#1a0900', bgTo: '#200c00', aspectRatio: 1.3 },
{ id: 5, title: 'Liminal', subtitle: 'Sculpture 3D, 2023', tag: '3D', accent: '#60a5fa', bgFrom: '#020d1a', bgTo: '#031628', aspectRatio: 1.55 },
{ id: 6, title: 'Vessel', subtitle: 'Ink & Code, 2024', tag: 'Ink', accent: '#fbbf24', bgFrom: '#1a1000', bgTo: '#201500', aspectRatio: 1.35 },
{ id: 7, title: 'Aether', subtitle: 'Watercolour, 2023', tag: 'Traditional', accent: '#c084fc', bgFrom: '#0d0520', bgTo: '#140628', aspectRatio: 1.5 },
{ id: 8, title: 'Cascade', subtitle: 'AI Assisted, 2024', tag: 'AI Art', accent: '#2dd4bf', bgFrom: '#021814', bgTo: '#032018', aspectRatio: 1.4 },
];
var panelStyle = {
position: 'fixed',
top: 0,
right: 0,
width: '340px',
height: '100vh',
background: 'rgba(8, 8, 16, 0.96)',
backdropFilter: 'blur(20px)',
borderLeft: '1px solid rgba(255,255,255,0.08)',
padding: '40px 28px',
zIndex: 500,
display: 'flex',
flexDirection: 'column',
gap: '16px',
color: '#fff',
fontFamily: 'sans-serif',
};
export default function ArtGallery() {
var [selected, setSelected] = useState(null);
return (
<div style={{ width: '100vw', height: '100vh' }}>
<AwesomeGalleryInfinity
cards={artworks}
columns={4}
defaultMode="Panorama"
onCardClick={function(card) { setSelected(card); }}
/>
{selected && (
<div style={panelStyle}>
<button
onClick={function() { setSelected(null); }}
style={{
alignSelf: 'flex-end', background: 'transparent',
border: '1px solid rgba(255,255,255,0.15)',
color: '#fff', borderRadius: '50%',
width: 32, height: 32, cursor: 'pointer', fontSize: 16,
}}
>×</button>
{/* Colour swatch */}
<div style={{
height: 120, borderRadius: 12,
background: 'linear-gradient(145deg, ' + selected.bgFrom + ', ' + selected.bgTo + ')',
border: '1px solid ' + selected.accent + '44',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<span style={{ fontSize: 52, fontWeight: 700, color: selected.accent, opacity: 0.3 }}>
{selected.title.slice(0, 2).toUpperCase()}
</span>
</div>
{/* Tag */}
<span style={{
alignSelf: 'flex-start',
background: selected.accent + '22',
border: '1px solid ' + selected.accent + '55',
color: selected.accent,
borderRadius: 100, padding: '3px 12px',
fontSize: 10, fontWeight: 700, letterSpacing: '0.15em', textTransform: 'uppercase',
}}>{selected.tag}</span>
{/* Title */}
<div style={{ fontSize: 28, fontWeight: 700, lineHeight: 1.1 }}>{selected.title}</div>
<div style={{ fontSize: 13, color: selected.accent, opacity: 0.85 }}>{selected.subtitle}</div>
<button style={{
marginTop: 'auto', padding: '12px 0', borderRadius: 8,
background: selected.accent, border: 'none', cursor: 'pointer',
fontWeight: 700, fontSize: 13, letterSpacing: '0.08em',
color: '#000', textTransform: 'uppercase',
}}>
View Artwork
</button>
</div>
)}
</div>
);
}Controls
| Input | Action |
|---|---|
| Scroll wheel | Speed up / slow down / reverse scroll direction |
| Drag up | Scroll faster upward |
| Drag down | Scroll downward |
| ↑ Arrow key | Decrease scroll speed |
| ↓ Arrow key | Increase scroll speed |
| Hover any card | Pause all columns |
| Click a card | Open detail modal (or fire onCardClick) |
| Escape | Close the detail modal |
CSS Custom Properties
Override fonts globally:
:root {
--font-display: 'Your Display Font', sans-serif;
--font-ui: 'Your UI Font', sans-serif;
}📄 License
MIT
