Skip to content
Dave Klein

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.json

DKMediaViewer 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.json

Give 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.

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

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 */
}

MIT licensed.