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.jsonOr 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.
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.
Loading image
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.
Loading image
API Reference
Import ImageViewerProps from the same file as ImageViewer.
| Prop | Type | Default |
|---|---|---|
slices | readonly ImageSlice[]The image slices, each with its width, height and pixel values. | Required |
slice | numberZero-based index of the shown slice, when controlled. | — |
defaultSlice | numberThe slice shown first, when uncontrolled. | 0 |
onSliceChange | (index: number) => voidCalled with the new slice index. | — |
window | ImageWindowThe window centre and width, when controlled. | — |
defaultWindow | ImageWindowDefaults to the first preset, or else the first slice's value range. | — |
onWindowChange | (window: ImageWindow) => voidCalled with the new window after a drag or preset. | — |
presets | readonly WindowPreset[]Named windows offered in the toolbar, such as `ctWindowPresets`. | [] |
pixelSpacing | readonly [number, number]Physical [row, column] spacing, as in DICOM Pixel Spacing, so pixels keep their true aspect. | — |
unit | stringUnit shown after pixel values, such as "HU". | — |
controls | booleanShows the toolbar and slice slider. Defaults to true. | true |
label | stringThe accessible name of the image. | "Medical image" |
className | stringClasses for the root element. | — |
ref | React.Ref<ImageViewerHandle>Moves through slices, zooms and resets the view from your own controls. | — |