dkmediaviewer
v0.1.0
Published
A minimalist, performant, and elegant photo and video viewer for React, extracted from diklein.com. Masonry grid, fly-in lightbox, a crossfading carousel, captions and camera EXIF data, light and dark modes, reduced-motion support, and one easter egg. Dis
Downloads
111
Maintainers
Readme
DKMediaViewer
DKMediaViewer is a minimalist, performant, and elegant photo and video viewer for React that was extracted from diklein.com. I am releasing it to share the work that went into making this UX delightful, and to let anyone give their own photos and videos the same experience.
Features:
- Masonry grid
- Fly-in lightbox with swipe and arrow-key navigation
- A crossfading carousel component
- Captions and camera EXIF data
- Light and dark modes
- Reduced-motion support
- Oh... and one easter egg
The DK prefix is of course an homage to NeXTSTEP's NS prefixes.
Live demo & docs: diklein.com/dkmediaviewer
Install
The viewer is distributed through the shadcn registry. The CLI drops the source code right into your project.
npx shadcn@latest add https://diklein.com/r/dk-media-viewer.jsonReact 18+ and Tailwind CSS v4 are required. The only npm dependency it brings in is motion.
Use
import { DKMediaViewer } from '@/components/dk-media-viewer/dk-media-viewer'
import items from '@/lib/media-items.json'
export default function Gallery() {
return <DKMediaViewer items={items} />
}An item is just:
{
src: '/photos/golden-gate.jpg',
alt: 'Golden Gate Bridge in fog',
caption: 'Marin Headlands, October.', // optional: shown in the lightbox rail
width: 2048, height: 1365, // optional: avoids layout shift
exif: { // optional: the mono spec line
make: 'Ricoh', model: 'GR IIIx',
exposureTime: '1/500', aperture: '2.8', focalLength: '26.1', iso: 200,
},
videoSrc: '/photos/clip.mp4', // optional: makes it a video (src = poster)
}Optional data like EXIF and caption degrade elegantly.
You can also point DKMediaViewer at a folder
You don't have to write media-items.json yourself. Point the CLI at your photo folder and it generates the file for you. Dimensions and camera settings come from the EXIF embedded in each image.
npx dkmediaviewer scan ./public/photos --out src/lib/media-items.jsonVideos need a poster image because it gives a video something to show instantly (in the grid, mid-animation, or when a device blocks autoplay) instead of a black box while the video loads. Give a video and an image the same name, like clip.mp4 and clip.jpg, and the scanner joins them into a single video item. The clip plays and the image is its poster frame.
Use two files per photo for high resolution
The grid and the lightbox share src, so a single reasonably large image works out of the box. The browser scales it down in the grid and shows it full size in the lightbox.
When your originals are heavy use two sizes. The grid loads a small file and the lightbox upgrades to the large one. The upgrade never delays the open because the lightbox always opens by animating the image already on your screen and then fades in the sharper version once the animation has landed.
If your images live on a CDN or behind an image optimizer, point src at a small render and rewrite it in getHiResSrc:
<DKMediaViewer
items={items}
getHiResSrc={(item) => item.src.replace('w=800', 'w=2048')}
/>On Next.js, pass renderImage and let next/image generate the small grid variants from the same large file:
<DKMediaViewer
items={items}
renderImage={(item, ctx) => (
<Image
src={item.src}
alt={item.alt ?? ''}
width={item.width}
height={item.height}
sizes={ctx.sizes}
className={ctx.className}
priority={ctx.priority}
/>
)}
/>With plain files keep a folder of thumbnails beside the originals and map between them:
<DKMediaViewer
items={items} // src points at photos/thumbs/
getHiResSrc={(item) => item.src.replace('/thumbs/', '/full/')}
/>The carousel
A second component, DKCarousel, runs the same items as a crossfading slideshow that auto-advances. Click a slide and the lightbox opens over the whole set, navigable with the same arrows.
import { DKCarousel } from '@/components/dk-media-viewer/dk-carousel'
<DKCarousel items={items} ratio="3 / 2" />The details
- The open animation starts from the thumbnail: The lightbox animates the image already on your screen into place instead of fetching a new copy mid-flight. No re-fetch, no flash.
- Lightbox navigation: swipe with axis-lock and rubber-banding at the ends. Arrow keys and on-screen controls can also switch assets. A swipe down will dismiss the lightbox on mobile.
- A real modal: Everything behind the modal goes
inertand focus enters the dialog. TheTabkey cycles controls, and a close hands focus back to the item you were viewing (with a focus ring appearing only for keyboard users). - Light and dark mode via the
.darkclass (shadcn convention), falling back toprefers-color-scheme. Every color is a--dk-*variable. prefers-reduced-motionis honored everywhere: no autoplay, no shimmer sweep, and instant transitions.- Videos keep playing through the open animation and hand off frame-accurately to the modal player. This was inspired by the minimize feature Steve Jobs demonstrated in the Mac OS X introduction in 2000.
The easter egg
Hold the Shift key while clicking a photo (or while pressing an arrow key inside the lightbox) and the animation runs at one-tenth speed. Mac OS X 10.3 shipped shift-click-minimize as a slow-motion genie effect, and Exposé kept the tradition. Some of us never got over it.
Theming
.dk-scope {
--dk-accent: #0f62fe; /* video progress bar, focus rings */
--dk-font-mono: 'Berkeley Mono', ui-monospace, monospace; /* the EXIF line */
}License
MIT © David Klein
