More Modals Snippets
Instagram Gallery Lightbox — Free HTML CSS JS Snippet
Instagram Gallery Lightbox · Modals · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Instagram Gallery Lightbox — Square Grid with a Full Photo Viewer

This snippet recreates the Instagram profile experience: a tidy three-column grid of square photos that show like counts on hover, and a full-screen lightbox that opens on click with previous/next navigation, keyboard arrows, swipe gestures, and the usual dismissal patterns. It's built in plain HTML, CSS, and vanilla JavaScript, with accessibility and touch handled.
The square grid
The grid is a CSS grid-template-columns: repeat(3, 1fr) with a tight 5px gap, exactly like Instagram's profile layout. Each cell uses aspect-ratio: 1 so it stays perfectly square regardless of column width, and overflow: hidden with a cover-sized background keeps images cropped to the square without distortion. On hover, a dark scrim fades in (::after) and a centered overlay reveals the photo's like count and tag — the signature Instagram hover, which surfaces engagement stats without cluttering the resting grid.
Data-driven cells
All photos live in a PHOTOS array (here, gradient placeholders with like counts and tags, swappable for real image URLs). The grid is generated in a loop, with each cell's background set through a --bg custom property and its like count formatted with toLocaleString() so large numbers read as "3,402". Clicking a cell opens the lightbox at that index.
The lightbox
The viewer is a fixed-position overlay with a blurred, dimmed backdrop (backdrop-filter: blur). It toggles via an .open class that transitions opacity and visibility together — using visibility ensures the closed lightbox isn't focusable or interactive, which a plain opacity fade wouldn't guarantee. Inside, a large square photo shows the current image, with a caption row displaying the like count and the position indicator ("3 / 9").
Smooth image swaps
When you navigate, render() briefly fades the photo to opacity: 0, swaps the --bg after 120ms, then fades back in — so moving between images cross-dissolves instead of hard-cutting. The go(d) function wraps the index with modulo arithmetic ((current + d + length) % length), so Next from the last photo loops to the first and Previous from the first loops to the last.
Every dismissal and navigation path
The lightbox supports the full set of expected interactions: the close button, clicking the backdrop (but not the photo, via an e.target === box check), the Escape key, the on-screen prev/next arrows, and the Left/Right arrow keys. On touch, touchstart/touchend measure horizontal swipe distance and call go() past a 50px threshold, so you can flick between photos on a phone. On small screens the arrows reposition to overlay the photo edges.
Focus management
When the lightbox opens it stores the previously focused element, moves focus to the Next button, and restores focus to the original trigger on close — the correct focus pattern for a modal dialog, which is marked up with role="dialog" and aria-modal="true".
Customizing it
Replace the gradient placeholders with real image URLs in PHOTOS, change the grid to four columns, adjust the swipe threshold, or add captions and avatars to the lightbox. Pair it with a focus cards section or a photo gallery elsewhere on the page for a complete media experience.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA three-column square photo grid renders like an Instagram profile.
- 2Hover a tileA scrim fades in showing the like count and tag.
- 3Click a photoA blurred full-screen lightbox opens on that image.
- 4NavigateUse the arrows, Left/Right keys, or swipe to move between photos.
- 5Close itClick the backdrop, press the X, or hit Escape.
- 6Swap in real photosReplace the PHOTOS array with image URLs.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Each cell uses aspect-ratio: 1, so its height always matches its width no matter how the three flexible columns resize. overflow: hidden with a cover-sized background crops images into the square without stretching them, reproducing Instagram's tidy profile grid at any viewport width.
The go(d) function computes (current + d + length) % length. The modulo wraps the index, so pressing Next on the last photo lands on the first and Previous on the first lands on the last. The same function backs the arrow buttons, the Left/Right keys, and swipe gestures.
Fading opacity alone leaves the overlay technically present and focusable while invisible, so keyboard users could tab into hidden controls. Transitioning visibility alongside opacity makes the closed lightbox non-interactive and removes it from the tab order, while still allowing the fade because visibility is transitionable when paired with a delay.
It's marked up as role="dialog" with aria-modal="true". On open it records the previously focused element and moves focus to the Next button; on close it restores focus to the trigger. It closes on Escape and on backdrop click, and all controls have aria-labels. For production you'd also trap Tab focus within the dialog while it's open.
Render the grid from your photos array and keep current index and open state in component state. Move the keyboard and touch listeners into a mount effect with cleanup, and use refs for focus management. Drive the lightbox visibility from state rather than toggling a class. Swap the gradient placeholders for <img> elements with real src values. In Tailwind, use aspect-square, grid-cols-3, and backdrop-blur utilities.