Skip to content
Dave Klein

DKMediaViewer

DKMediaViewer handles both the grid and lightbox on my photos page, and now it is available to download as a reusable component. It starts with a masonry grid, and when a photo is clicked it animates into a modal that supports swipe and arrow-key navigation. The DK prefix 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 @diklein/dk-media-viewer

DKMediaViewer is distributed through the shadcn registry. The CLI places the source code into your project, and there is no package to depend on. React 18+ and Tailwind CSS v4 are required.

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

Everything after src is optional and degrades elegantly.

media-items.json
{
  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)
  blurHash: 'LEHV6nWB2yk8pyo0adR*.7kCMdnj',  // optional: a blurred preview while it loads
}

You can also point it at a folder

Point the CLI at a folder filled with photos, and media-items.json is generated automatically.

npx @diklein/dkmediaviewer scan ./public/photos --out src/lib/media-items.json

Use two files per photo for high resolution

The grid and the lightbox share src, so a somewhat large image just works. If a photo is gigantic, use two sizes. The grid will show the small file and the lightbox will show the large one. The lightbox secretly fades in the larger image as soon as it’s ready.

// 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/')}
/>

A second component, DKCarousel, provides you with a crossfading slideshow that auto-advances. Click a slide to view the series of photos in the lightbox.

Mirror

Details

  • —The lightbox swaps to a larger image if available without any flashes.
  • —The modal follows strict accessibility requirements.
  • —Light and dark modes are supported using the .dark class.
  • —prefers-reduced-motion is honored everywhere to prevent autoplay and introduce instant transitions.
  • —Videos keep playing through the open animation. This was inspired by the minimize feature Steve Jobs demonstrated in the Mac OS X introduction in 2000.

Easter egg

Press a Shift key and click on a photo (or arrow key inside the lightbox), and the animation runs at one-tenth speed. When Mac OS X 10.3 launched, the similar feature to make dock minimizing move in slow motion expanded to support Exposé. It was extremely cool. Try it on the grid above.

Theming

globals.css
.dk-scope {
  --dk-accent: #0f62fe;                /* video progress bar */
  --dk-font-mono: 'Berkeley Mono', ui-monospace, monospace;  /* the EXIF line */
}

MIT licensed.