component 23 / 28WORKING
SC Gallery Lightbox
- type
- Code Component
- platform
- Framer
- license
- SecretoCho Design
demo · runs with JavaScript
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
Code comment
// ScGalleryUnified_Fixed.tsx - SecretoCho™ Unified Gallery Component// Clean version with Gallery Name, Swipe Support, and all errors fixed// ready_to_ship=trueDependencies
- 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
- Drop ScGalleryUnified into a frame and name the gallery.
- 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.
- Display: columns, gap, aspect ratio, radius, shadow, backgrounds, hover scale and border.
- Lightbox: enable, animation (fade / slide / scale), background and blur, gallery name, zoom, fullscreen; style the Button Style, Top Bar and Thumbnails.
- 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
| name | type | default | description |
|---|---|---|---|
galleryName | string | Gallery | Gallery Name. Shown in the lightbox top bar when enabled. |
dataMode | enum | manual | Data Mode. Manual | Collection (segmented). |
images | array | three sample images (Breakfast, Burger, Camera from unsplash.com) | Images. Array of {src Image, alt, caption, thumbnailSrc Image}; shown in manual mode. |
collectionList | ComponentInstance | — | Collection List. Connect a Framer Collection List; shown in collection mode. |
display | object | columns=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=#00E183 | Display. 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. |
buttonStyle | object | borderWidth=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=true | Button 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). |
lightbox | object | enabled=true, animation=scale, bgColor=rgba(0, 0, 0, 0.95), bgBlur=false, showGalleryName=true, enableZoom=true, enableFullscreen=true | Lightbox. Enabled; fade | slide | scale; background and blur; gallery name; zoom; fullscreen. |
topBar | object | bgColor=rgba(0, 0, 0, 0.3), textColor=#FFFFFF, backdropBlur=true | Top Bar. Background, text colour, blur. |
thumbnails | object | show=true, height=60, gap=8, bgColor=rgba(0, 0, 0, 0.5), borderColor=transparent, activeColor=#00E183 | Thumbnails. Strip at the bottom; height 40–100, gap 4–16; active colour also colours the dots. |
navigation | object | arrows=true, dots=false, counter=true, hints=true, keyboard=true, loop=true | Navigation. Dots show only when thumbnails are off. |
slideshow | object | autoPlay=false, interval=5, pauseOnHover=true | Slideshow. Interval 2–10 s (see the note under this table). |
performance | object | lazyLoad=true, preloadCount=2, maxItems=50 | Performance. 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
// ScGalleryUnified_Fixed.tsx - SecretoCho™ Unified Gallery Component// Clean version with Gallery Name, Swipe Support, and all errors fixed// ready_to_ship=trueKey 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.
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
Provided “as is” for visual/UX purposes. Validate performance, accessibility, and licensing within your project.
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