Components / Imaging

Medical image viewer

Scroll through a stack of CT or MRI slices, adjust window and level, zoom and pan, and read uncompressed DICOM files in the browser.

Installation

In a project set up as in Installation, run:

bunx shadcn@4.22.1 add https://organui.com/r/image-viewer.json

Or by name: bunx shadcn@4.22.1 add @organui/image-viewer.

The viewer installs into your configured UI directory, with the DICOM reader and sample phantom in lib/dicom.ts and lib/head-phantom.ts. shadcn's alert, button, progress, select, slider, toggle and tooltip come with it if you do not have them yet. It draws on a 2D canvas and adds no imaging packages.

View registry item JSON

Usage

"use client"
import { useState } from "react"
import { ImageViewer, ctWindowPresets } from "@/components/ui/image-viewer"
import { createHeadPhantom } from "@/lib/head-phantom"

const phantom = createHeadPhantom()

export function Scan() {
  const [slice, setSlice] = useState(15)
  return <ImageViewer {...phantom}
    presets={ctWindowPresets}
    slice={slice} onSliceChange={setSlice} />
}

Each slice is either raw values with their size, such as Hounsfield units in an Int16Array or Float32Array in row-major order, or an image URL. Image pixels become their luminance from 0 to 255; serve them from your own origin or with CORS so the viewer can read them. Pass pixelSpacing as [row, column] to keep non-square pixels in proportion, and unit to label values under the pointer.

<ImageViewer slices={[
  { src: "/scans/axial-001.png", label: "Axial 1" },
  { src: "/scans/axial-002.png", label: "Axial 2" },
]} />

Window and level

The window maps values from center - width / 2 to center + width / 2 onto black through white, using the DICOM linear function. Drag across the image to change it: right widens the window and down raises the level. Pass presets, such as the included brain, soft tissue, lung and bone ctWindowPresets, to offer named windows. Without a window or presets the viewer starts from the first slice's range.

Slice and window can each be controlled with slice and window plus their change callbacks, or left to the component with defaultSlice and defaultWindow. A ref exposes setSlice, zoom and reset.

DICOM files

import { dicomSeries, parseDicom } from "@/lib/dicom"

async function openFiles(files: FileList) {
  const images = await Promise.all(
    [...files].map(async (file) => parseDicom(await file.arrayBuffer()))
  )
  // { slices, pixelSpacing, window, unit }, ordered head to foot
  return dicomSeries(images)
}

parseDicom reads single-frame grayscale Part 10 files in implicit or explicit little endian, applies rescale slope and intercept, and returns the stored window, pixel spacing and patient position. dicomSeries orders a series from head to foot. Compressed transfer syntaxes, color and multi-frame images throw a readable error; use a full DICOM toolkit for those.

Keyboard and pointer

Tab focuses the image. Up and Down move one slice, Page Up and Page Down move ten, and Home and End jump to the first and last. Plus and minus zoom and zero resets the view. Scrolling moves through slices; ctrl or a trackpad pinch zooms at the pointer, as does a two-finger pinch on touch screens. Drag adjusts the window, or pans with the pan tool, shift or the middle button. The slice slider below the image works with the keyboard too.

For education and interface prototyping. It is not a medical device and is not validated for diagnosis. Keyboard and browser checks do not replace a screen-reader usability audit.