Skip to content
Black FridayBlack Friday launch price: Lifetime $149 until 30 Nov. Regular price $299 from 1 Dec.ends in 15d 03:13:36Get Lifetime →
loopkit
proNew380 lines · framer-motion

Image Showcase Lightbox

A grid of generated artworks that opens into its own viewer. A full-width card grid of generated abstract artworks (pure CSS gradients + SVG, no external images) that opens a custom lightbox on click, with Prev/Next, keyboard navigation and a focus trap.

Use it for
A portfolio, case-study or gallery section where tiles need to open into a focused, distraction-free viewer instead of linking off the page.
Themes
Light + dark
License
Commercial use
Live

Field notes

A grid of studies, each one worth a closer look

tsx
"use client";

import * as React from "react";
import { createPortal } from "react-dom";
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
import { cn } from "@/lib/utils";

/**
 * Full-width "Cards" showcase: a responsive grid of generated abstract artworks
 * (pure CSS gradients + SVG, no external images, deterministic per index — no
 * Math.random/Date.now in the render path). Clicking a tile opens a custom
 * lightbox rendered via createPortal to document.body with Prev/Next, a close
 * button, backdrop-click-to-close, Escape-to-close, arrow-key navigation, a
 * simple focus trap, and framer-motion AnimatePresence transitions.
 */

export interface ShowcaseTile {
  caption: string;
  /** Deterministic seed driving the generated artwork's hues/angles. Defaults to the tile's index. */
  seed?: number;
}

The rest is in Pro.

Full source, the rebuild prompt and registry install unlock with Pro or Lifetime.

Get Pro

How it behaves

  • →Each tile's artwork is generated purely from its index: hue, gradient angle and an SVG blob position/polygon are all derived from seed ?? index with fixed arithmetic (golden-angle hue stepping), so there is no Math.random/Date.now and the server/client markup always matches.
  • →Clicking a tile stores a ref to the trigger button, then opens a lightbox rendered via createPortal into document.body so it always paints above the grid even when this block sits inside a small, scaled preview container.
  • →The lightbox mounts only after a client-side effect flips a mounted flag, so createPortal/document are never touched during server rendering.
  • →Open/close is animated with framer-motion AnimatePresence (backdrop fade + panel scale/slide); switching images crossfades/slides the artwork left or right with AnimatePresence mode="wait" driven by a direction value set on Prev/Next.
  • →A single keydown listener (added only while open, removed on close/unmount) handles Escape to close, ArrowLeft/ArrowRight to navigate, and Tab/Shift+Tab to cycle focus between the dialog's focusable elements (a manual focus trap, no extra dependency).
  • →Clicking the backdrop (but not the panel itself) closes the viewer; focus moves to the close button on open and returns to the tile that was clicked on close.
  • →document.body.style.overflow is set to hidden while open and always restored from the previous value in the effect's cleanup, even on fast open/close or unmount.
  • →columns and aspect control the grid's minmax() column width and each tile's aspect ratio (square / portrait / landscape), reused by the lightbox artwork so the enlarged image keeps the same proportions.

Accessibility

  • ✓The lightbox panel is `role="dialog"` with `aria-modal="true"` and an `aria-label` naming the current image and its position (e.g. "image 3 of 8").
  • ✓Opening moves focus to the close button; closing returns focus to the exact tile button that was activated, so keyboard users never lose their place in the grid.
  • ✓Tab and Shift+Tab are trapped inside the dialog's focusable controls (close, previous, next) while it is open.
  • ✓All interactive elements are real `<button>`s with descriptive `aria-label`s ("Open … in the image viewer", "Previous image", "Next image", "Close image viewer"); decorative SVG icons and overlays are `aria-hidden`.
  • ✓Honours prefers-reduced-motion.