Skip to content

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.

Accessibility

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.

Examples

Controlled Slice and Window

Slice and window come from your state, so your own controls can drive them.

Use the up and down arrow keys or scroll to move through slices, and plus, minus and zero to zoom and reset. Drag to adjust the window, or to pan when the pan tool is on.

x

Loading image

Slice
16 / 32

Without Controls

No toolbar or slider; the image still scrolls, zooms and windows.

Use the up and down arrow keys or scroll to move through slices, and plus, minus and zero to zoom and reset. Drag to adjust the window, or to pan when the pan tool is on.

x

Loading image

API Reference

Import ImageViewerProps from the same file as ImageViewer.

ImageViewer props
PropTypeDefault
slicesreadonly ImageSlice[]

The image slices, each with its width, height and pixel values.

Required
slicenumber

Zero-based index of the shown slice, when controlled.

—
defaultSlicenumber

The slice shown first, when uncontrolled.

0
onSliceChange(index: number) => void

Called with the new slice index.

—
windowImageWindow

The window centre and width, when controlled.

—
defaultWindowImageWindow

Defaults to the first preset, or else the first slice's value range.

—
onWindowChange(window: ImageWindow) => void

Called with the new window after a drag or preset.

—
presetsreadonly WindowPreset[]

Named windows offered in the toolbar, such as `ctWindowPresets`.

[]
pixelSpacingreadonly [number, number]

Physical [row, column] spacing, as in DICOM Pixel Spacing, so pixels keep their true aspect.

—
unitstring

Unit shown after pixel values, such as "HU".

—
controlsboolean

Shows the toolbar and slice slider. Defaults to true.

true
labelstring

The accessible name of the image.

"Medical image"
classNamestring

Classes for the root element.

—
refReact.Ref<ImageViewerHandle>

Moves through slices, zooms and resets the view from your own controls.

—