SecretoCho
menu

component 23 / 28WORKING

SC Gallery Lightbox

type
Code Component
platform
Framer
license
SecretoCho Design
needs JavaScript

demo · runs with JavaScript

fig 00 · live demo, ported from ScGalleryLightbox.tsx

Grid gallery with a portal lightbox: manual or Collection List images, swipe/pinch/zoom/drag, keyboard, thumbnails, slideshow, themed lightbox buttons.

ScGalleryUnified is an image grid with a full-screen lightbox. Images come from a manual list or from a connected Framer Collection List, whose rendered tree the component walks to find every image and its alt text. The lightbox has arrows, keyboard, swipe and pinch, zoom to 3×, drag to pan, fullscreen, thumbnails or dots, a counter, hints and an optional slideshow; button, bar and thumbnail colours are controls; only the inactive dots are fixed at rgba(255, 255, 255, 0.3).

  • Media
  • UI
  • Navigation

Main

Overview

Built With
React (hooks), react-dom createPortal, framer-motion, Fullscreen API, Framer Code Component API
Layout Support
Grid fills the frame width (1–6 columns); the lightbox is a fixed portal on document.body
Orientation
Grid tiles with an aspect ratio; lightbox with a top bar, centred image, bottom hints and thumbnails
Ideal Use Case
Case-study galleries, portfolio grids, CMS image collections

Tags

  • gallery
  • lightbox
  • portal
  • swipe
  • pinch zoom
  • thumbnails
  • slideshow
  • collection list
  • framer-motion
  • fullscreen

Code comment

ScGalleryLightbox.tsx · lines 1–3 · the file’s header comment
// ScGalleryUnified_Fixed.tsx - SecretoCho™ Unified Gallery Component// Clean version with Gallery Name, Swipe Support, and all errors fixed// ready_to_ship=true

Dependencies

Technologies
framer-motion, react-dom (both in the Framer runtime)
Notes
Collection mode needs a Framer Collection List connected to the Collection List control; it reads the rendered React elements, not the CMS API.

Complexity

Logic Level
High (image extraction from a React tree, navigation, zoom/drag/swipe/pinch state machines, slideshow, canvas detection)
Runtime Cost
Low at rest; medium while the lightbox animates (framer-motion transitions, preloading neighbours)
Component Size
~102 KB TSX (101,806 bytes, 2,377 lines)
Notes
Eight object control groups (display, buttonStyle, lightbox, topBar, thumbnails, navigation, slideshow, performance) plus the Images array of objects; the lightbox is rendered only while open.

Guide

Instructions

  1. Drop ScGalleryUnified into a frame and name the gallery.
  2. Data Mode: Manual — add images (Image, Alt Text, Thumbnail; Caption is stored but not shown) to the Images array; Collection — connect a Framer Collection List to the Collection List control.
  3. Display: columns, gap, aspect ratio, radius, shadow, backgrounds, hover scale and border.
  4. Lightbox: enable, animation (fade / slide / scale), background and blur, gallery name, zoom, fullscreen; style the Button Style, Top Bar and Thumbnails.
  5. Navigation: arrows, dots, counter, hints, keyboard, loop; Slideshow: autoplay and interval; Performance: lazy load, preload count, max items.

Examples / Presets

  • Default

    The source defaults: 3 columns, square tiles, dark lightbox, scale animation, thumbnails, arrows, counter and hints.

  • As on the live site

    display.columns=2, display.borderRadius=0, buttonStyle.borderWidth=1, lightbox.bgBlur=true, navigation.dots=true, slideshow.interval=4, performance.maxItems=16 (the old site’s case-study instances; their exact colours come from the demo module). navigation.dots=true has no visible effect there: dots render only when thumbnails.show is false, and these instances keep the thumbnails on; the colour values listed are the source defaults, the live instances use colour tokens

  • Slide with dots

    lightbox.animation
    slide
    thumbnails.show
    false
    navigation.dots
    true
    display.aspectRatio
    landscape
    display.borderRadius
    12
    display.hoverBorderWidth
    2
    navigation.hints
    false (with hints on, the hints bar at bottom 20 px sits under the dots at bottom 40 px)
  • Fade, minimal

    lightbox.animation
    fade
    lightbox.enableZoom
    false
    lightbox.enableFullscreen
    false
    thumbnails.show
    false
    navigation.counter
    false
    navigation.hints
    false
    buttonStyle.borderStyle
    solid
    buttonStyle.borderWidth
    1

Object groups are given whole: a passed group replaces the default object and its keys are read with no per-key fallback, except buttonStyle, which falls back per key (a borderWidth of 0 becomes 2).

The live-site instances run in collection mode; the demo runs that preset in collection mode too, on a sample Collection List of its own images, so the extractor runs.

Mechanics

How it Works

  • Images: manual mode filters the Images array to items with a src; collection mode runs ImageExtractor.collectDeep over the Collection List’s React children (depth ≤ 10), treating as images: <img>, as / tag “img”, a background src or image, an image prop, a style backgroundImage, __framer__image, data-framer-background-image-wrapper, and element types or class names containing “image” (matches without a src are skipped) and reading alt from alt / title / aria-label / name / description or child text; results are de-duplicated by src and cut to Max Items.
  • Grid: CSS grid of motion.div tiles with the chosen aspect ratio; lazy-loaded <img>s; hover scale and border; the first tile (all tiles on the canvas) shows the expand icon.
  • Open: scrollY is stored, the body fixed at −scrollY, and the lightbox rendered through createPortal into document.body under AnimatePresence; close clears those body styles and scrolls back; the portal unmounts at once, so the exit animations do not play.
  • Navigate: arrows, ← → keys, swipe (50 px or velocity 0.3 px/ms), thumbnails or dots, loop or clamp; the current and preloadCount neighbours are preloaded with new Image().
  • Zoom: 0.5–3×. The buttons, the + / = and − / _ keys and ctrl/cmd + wheel step by 0.25 and 0 resets, all behind Enable Zoom; pinch scales freely between 0.5 and 3× and works even with Enable Zoom off. Above 1× the image can be dragged within clamped offsets.
  • Slideshow: a setInterval of interval seconds advances while open and playing; play/pause lives in the top bar. Auto Play only sets the play state when the component mounts; closing turns play off, so later openings start paused.
  • Canvas: RenderTarget / URL checks detect the Framer canvas and show placeholders, sample images and alt debug labels there.

Performance & Tuning

  • Lazy Load adds loading="lazy" to the grid images; each tile’s onLoad also preloads its full src and Preload Count (0–5) neighbours on each side, so the grid fetches full-size images beyond those in view.
  • Max Items caps very large collections; the extractor stops at depth 10.
  • Backdrop blur on the lightbox, top bar and buttons costs compositing: turn bgBlur / backdropBlur off on weak devices (the thumbnail strip’s blur(10px) is fixed).
  • Thumbnails render every image again as a small <img>; supply thumbnailSrc for big files.

Compatibility & Limits

  • Evergreen browsers; the fullscreen toggle calls requestFullscreen with no feature check, and nothing listens for fullscreenchange, so leaving fullscreen other than by the button leaves its icon and label unchanged; framer-motion and react-dom come with Framer.
  • Collection mode depends on how Framer renders its Collection List; a structure the extractor does not recognize yields no images.
  • isMobile is window.innerWidth < 768 measured once on mount; zoom and fullscreen buttons are hidden on mobile.
  • Slideshow’s Pause on Hover and the image Caption are controls the current code does not read.

Quality

Accessibility

  • The arrow, zoom, play/pause, fullscreen and close controls are <button>s with aria-labels (Previous, Next, Zoom out, Zoom in, Play / Pause, Enter / Exit fullscreen, Close). Thumbnail buttons are named only by their image’s alt, and the dots have no label. Grid tiles are plain divs with a click handler: they cannot be focused or opened from the keyboard.
  • Keyboard, only while open and with Navigation › Keyboard on: ← → navigate and Esc closes; with Enable Zoom on, + / = and − / _ zoom and 0 resets; with Enable Fullscreen on, F toggles fullscreen. With Keyboard off, Esc does nothing.
  • Missing alt falls back to “Gallery image N” in the grid and “Image N” in the lightbox; thumbnails use the alt as is. The lightbox container has no role="dialog" / aria-modal, focus is not trapped, and a click outside the image does not close it.

Security & Privacy

  • Network: only the image URLs the data provides (plus preloads of neighbours). The built-in sample images are three images.unsplash.com URLs: the default of the images prop (used whenever no list is passed) and a canvas-only fallback when the manual list is empty; live, an empty list shows “Add images to display”. No fetch, no tracking.
  • Permissions: the Fullscreen API on a click or the F key; nothing else.
  • No storage. dangerouslySetInnerHTML is used only for the file’s own inline SVG icon strings, never for data.
  • In collection mode on the canvas, the component logs the Collection List data and the extracted images to the console; its canvas test also matches a URL whose path contains /edit or whose query contains edit=true.
  • A window keydown listener exists while the lightbox is open with Keyboard on; document mousemove, mouseup, touchmove (non-passive) and touchend listeners, plus body user-select: none, exist only while a zoomed image is dragged, and are removed after. Body position, top and width are cleared only by closing: unmounting while open leaves the body fixed.

Reference

Props

nametypedefaultdescription
galleryNamestringGalleryGallery Name. Shown in the lightbox top bar when enabled.
dataModeenummanualData Mode. Manual | Collection (segmented).
imagesarraythree sample images (Breakfast, Burger, Camera from unsplash.com)Images. Array of {src Image, alt, caption, thumbnailSrc Image}; shown in manual mode.
collectionListComponentInstance—Collection List. Connect a Framer Collection List; shown in collection mode.
displayobjectcolumns=3, gap=12, aspectRatio=square, borderRadius=8, boxShadow=0 4px 14px rgba(0,0,0,.08), imageBackground=#000000, containerBackground=#FAFAFA, hoverEffect=true, hoverScale=1.02, hoverBorderWidth=0, hoverBorderColor=#00E183Display. Columns 1–6, gap 0–50, Auto | 1:1 | 16:9 | 3:4 | 4:3, radius 0–24, hover scale 1–1.1, hover border 0–10 px.
buttonStyleobjectborderWidth=2, borderStyle=dashed, borderColor=rgba(255, 255, 255, 0.3), borderHoverColor=#00E183, borderActiveColor=#00E183, bgColor=rgba(0, 0, 0, 0.5), bgHoverColor=rgba(0, 0, 0, 0.7), bgActiveColor=rgba(0, 225, 131, 0.2), iconColor=#FFFFFF, iconHoverColor=#FFFFFF, backdropBlur=trueButton Style: colours, border and blur of the arrow, zoom, play/pause, fullscreen and close buttons and the grid’s expand badge; the hints bar and zoom read-outs take only its border and blur. Border width 0–5 (0 falls back to 2 px).
lightboxobjectenabled=true, animation=scale, bgColor=rgba(0, 0, 0, 0.95), bgBlur=false, showGalleryName=true, enableZoom=true, enableFullscreen=trueLightbox. Enabled; fade | slide | scale; background and blur; gallery name; zoom; fullscreen.
topBarobjectbgColor=rgba(0, 0, 0, 0.3), textColor=#FFFFFF, backdropBlur=trueTop Bar. Background, text colour, blur.
thumbnailsobjectshow=true, height=60, gap=8, bgColor=rgba(0, 0, 0, 0.5), borderColor=transparent, activeColor=#00E183Thumbnails. Strip at the bottom; height 40–100, gap 4–16; active colour also colours the dots.
navigationobjectarrows=true, dots=false, counter=true, hints=true, keyboard=true, loop=trueNavigation. Dots show only when thumbnails are off.
slideshowobjectautoPlay=false, interval=5, pauseOnHover=trueSlideshow. Interval 2–10 s (see the note under this table).
performanceobjectlazyLoad=true, preloadCount=2, maxItems=50Performance. Lazy load; preload 0–5 neighbours on each side; max items 1–200.

slideshow.pauseOnHover (its type, default and control) and images[].caption (its type and control) are never read by the render or the effects. hoverBorderColor shows only when hoverBorderWidth is above 0.

Notes & Tips

  • Give every image an alt: the lightbox reads it, and the canvas shows “No alt text found” tooltips where it is missing.
  • Dots need Thumbnails off; the counter works with either.
  • Collection mode: set alt (or title, aria-label, name, description) on the image elements; the extractor reads them in that order, then the first text child.
  • For heavy files set thumbnailSrc: the grid and the strip use it, the lightbox the full src.

Troubleshooting / FAQ

Collection mode shows “Connect Collection List”

No Collection List is connected, or its rendered tree holds nothing the extractor recognizes as an image.

No zoom or fullscreen buttons on a phone

They are hidden under 768 px; pinch zoom still works.

The slideshow does not pause on hover

Pause on Hover is a control the code does not use.

The page scrolls back to the top after closing

Closing clears body position, top and width to empty values (not their earlier values) and scrolls the window back to the scrollY saved on open; there is no cleanup if the gallery unmounts while open.

Source

Header comment

ScGalleryLightbox.tsx · lines 1–3 · header comment
// ScGalleryUnified_Fixed.tsx - SecretoCho™ Unified Gallery Component// Clean version with Gallery Name, Swipe Support, and all errors fixed// ready_to_ship=true

props [REF]

Key excerpt

Collection mode has no data API: the gallery walks the Collection List’s rendered React tree, ten levels deep, and collects every element that looks like an image, keyed by src.

ScGalleryLightbox.tsx · lines 236–273 · tsx
    static collectDeep(        node: any,        collected: Map<string, GalleryImage>,        depth: number = 0    ): void {        if (!node || depth > 10) return        if (this.isImageElement(node)) {            const { src, alt } = this.extractFromFramerImage(node)            if (src && !collected.has(src)) {                let finalAlt = alt                if (!finalAlt && node.props) {                    finalAlt = this.extractTextFromChildren(node.props.children)                }                collected.set(src, { src, alt: finalAlt || "" })            }        }        const props = node?.props || node        if (props?.children) {            const children = React.Children.toArray(props.children)            for (const child of children) {                this.collectDeep(child, collected, depth + 1)            }        }        if (Array.isArray(node)) {            for (const item of node) {                this.collectDeep(item, collected, depth + 1)            }        }        if (props?.items && Array.isArray(props.items)) {            for (const item of props.items) {                this.collectDeep(item, collected, depth + 1)            }        }    }

Credits

Credits

© SecretoCho Design

license
SecretoCho Design

SecretoCho Design

Disclaimer

Proves

Hiring a design engineer?

I’m open to senior design engineer roles: remote from Belgrade, or relocation with visa sponsorship. Write to me, or read the CV first. Client work stays selective, one project at a time.

sc@secretocho.design

read the CV · LinkedIn