DKMediaViewer
DKMediaViewer is the media viewer running on my photos page, extracted into a minimalist, performant component anyone can install. 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. A masonry grid, a fly-in lightbox that animates your thumbnail into the modal with swipe and arrow-key navigation, a crossfading carousel, captions and camera EXIF data, light and dark modes, and one easter egg. The DKprefix is of course an homage to NeXTSTEP’s NS prefixes.
Try it
Click a photo and tap the ← → arrow keys. Tap esc, click on a photo, or click × to close.
Install
npx shadcn@latest add https://diklein.com/r/dk-media-viewer.jsonDKMediaViewer is distributed through the shadcn registry. The CLI drops the source code right into your project. There is no package to depend on. The files land in components/dk-media-viewer and they are yours to keep or change. React 18+ and Tailwind CSS v4 are required, and the only npm dependency it brings in is motion.
Use
import { DKMediaViewer } from '@/components/dk-media-viewer/dk-media-viewer'
import '@/components/dk-media-viewer/dk-media-viewer.css'
import items from '@/lib/media-items.json'
export default function Gallery() {
return <DKMediaViewer items={items} />
}An item is plain data. Everything beyond src is optional and degrades elegantly.
{
src: '/photos/golden-gate.jpg',
alt: 'Golden Gate Bridge in fog',
caption: 'Marin Headlands, October.',
width: 2048, height: 1365,
exif: {
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)
}You can also point it 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.jsonGive a video and a poster 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, so the item shows instantly in the grid and for anyone whose device blocks autoplay.
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. Point src at a small CDN render and rewrite it in getHiResSrc, let next/image generate the grid variants through renderImage, or keep a folder of thumbnails beside the originals:
// A CDN or image optimizer: rewrite the small render
<DKMediaViewer
items={items}
getHiResSrc={(item) => item.src.replace('w=800', 'w=2048')}
/>
// Plain files: photos/thumbs/ in the grid, photos/full/ in the lightbox
<DKMediaViewer
items={items}
getHiResSrc={(item) => item.src.replace('/thumbs/', '/full/')}
/>The demo above does both halves: the grid renders through next/image via renderImage, and getHiResSrc upgrades the lightbox to a 2048px Unsplash render.
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.
Omotesando Koffee is my favorite coffee shop in London (and Tokyo of course)
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.
- —A real modal. Everything behind the modal goes inert and focus enters the dialog. Tab 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 (the shadcn convention), falling back toprefers-color-scheme. Every color is a--dk-*variable, so a retheme is one selector. - —
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.
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. Try it on the grid above.
Theming
.dk-scope {
--dk-accent: #0f62fe; /* video progress bar */
--dk-font-mono: 'Berkeley Mono', ui-monospace, monospace; /* the EXIF line */
}