react-geotiff-viewer
v1.1.0
Published
A production-ready React component for viewing large TIFF files with WebGL tile rendering, zoom/pan/rotate controls, and auto band detection. No server required.
Maintainers
Readme
react-tiff-viewer
A production-ready React component for rendering large TIFF files directly in the browser — no backend, no server, no pre-processing required. Drop in a URL and get tile-based WebGL rendering with zoom, pan, and rotate controls out of the box.
Built by Aashir Shahid
How It Works
Your TIFF URL
│
▼
geotiff.js ← reads ~64 KB of the file header via HTTP range request
│ detects band count: 1 (grayscale), 3 (RGB), 4 (RGBA), etc.
▼
GeoTIFFSource ← OpenLayers source, streams only the tiles currently in view
│ works with both plain TIFFs and Cloud-Optimized GeoTIFFs
▼
WebGLTileLayer ← renders tiles on a WebGL canvas
│ applies correct RGB/RGBA/grayscale shader per band count
▼
OL View (COG-native resolutions)
← view is built from the TIFF's own tile pyramid resolutions
so zooming in always fetches higher-res tiles, never upscalesNo Python, no Node.js server, no file pre-processing. Everything runs in the browser.
Features
- Zero server required — just a URL
- Auto band detection — grayscale (1-band), RGB (3-band), RGBA (4-band), multi-band all render correctly
- No pixelation on zoom — view is built from the COG tile pyramid's own resolution levels
- HTTP range request streaming — only loads tiles visible in the current viewport
- High bit-depth support — uint8, uint16, float32, int16 — all normalized for display
- Zoom, pan, rotate — built-in floating toolbar or drive it with your own UI via
ref - Dark and light themes
- Full TypeScript — strict types, declaration files, complete IntelliSense
- Headless hook —
useTiffViewerfor fully custom UI - Dual bundle — ESM and UMD with source maps
Installation
npm install react-geotiff-viewer
# or
yarn add react-geotiff-viewer
# or
pnpm add react-geotiff-viewerPeer dependencies (already in most React projects):
npm install react react-domQuick Start
import { TiffViewer } from 'react-geotiff-viewer';
import 'react-geotiff-viewer/dist/react-tiff-viewer.css';
export default function App() {
return (
<TiffViewer
url="https://example.com/image.tiff"
height={600}
/>
);
}TIFF Format Support
| TIFF Type | Bands | What the viewer does | |-----------|-------|----------------------| | Grayscale | 1 | Channel duplicated across R, G, B | | Grayscale + Alpha | 2 | Grayscale with transparency | | RGB | 3 | Direct RGB mapping | | RGBA | 4 | RGB with alpha channel | | Multi-band (5+) | 5+ | First three bands mapped to RGB | | Cloud-Optimized GeoTIFF (COG) | any | Tile streaming — fastest, recommended | | Plain / unoptimized TIFF | any | Full file downloaded, then displayed | | High bit-depth (uint16, float32…) | any | Auto-normalized to display range |
Recommendation: For large files, pre-convert to COG format so only visible tiles are fetched. See Pre-converting to COG.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| url | string | required | URL to any TIFF file |
| height | number \| string | 600 | Container height — number is treated as px, string used as-is ('80vh') |
| width | number \| string | '100%' | Container width |
| showToolbar | boolean | true | Show the floating zoom/pan/rotate toolbar |
| onLoad | (info: TiffLoadInfo) => void | — | Fired once the TIFF has loaded |
| onError | (error: Error) => void | — | Fired on load failure |
| theme | 'dark' \| 'light' | 'dark' | UI color theme |
| initialZoom | 'fit' \| 'original' | 'fit' | 'fit' fits the whole image in view; 'original' starts at 1:1 pixels |
| className | string | — | CSS class added to the root element |
| style | React.CSSProperties | — | Inline styles on the root element |
Examples
With load callback
import { TiffViewer } from 'react-tiff-viewer';
import type { TiffLoadInfo } from 'react-tiff-viewer';
function App() {
function handleLoad(info: TiffLoadInfo) {
console.log(`${info.width} × ${info.height} px, ${info.bands} bands`);
}
return (
<TiffViewer
url="https://example.com/image.tiff"
onLoad={handleLoad}
onError={(err) => console.error('Failed:', err.message)}
/>
);
}Programmatic control via ref
import { useRef } from 'react';
import { TiffViewer } from 'react-tiff-viewer';
import type { TiffViewerHandle } from 'react-tiff-viewer';
function App() {
const ref = useRef<TiffViewerHandle>(null);
return (
<>
<button onClick={() => ref.current?.zoomIn()}>Zoom In</button>
<button onClick={() => ref.current?.zoomOut()}>Zoom Out</button>
<button onClick={() => ref.current?.zoomToFit()}>Fit</button>
<button onClick={() => ref.current?.rotate(90)}>Rotate 90°</button>
<button onClick={() => ref.current?.resetZoom()}>Reset</button>
<TiffViewer
ref={ref}
url="https://example.com/image.tiff"
showToolbar={false}
/>
</>
);
}Headless hook — fully custom UI
import { useTiffViewer } from 'react-tiff-viewer';
function CustomViewer({ url }: { url: string }) {
const {
mapContainerRef,
isLoading,
error,
loadInfo,
zoomIn,
zoomOut,
zoomToFit,
rotate,
} = useTiffViewer({ url });
return (
<div style={{ position: 'relative', width: '100%', height: 600 }}>
{/* OL renders into this div */}
<div ref={mapContainerRef} style={{ width: '100%', height: '100%' }} />
{isLoading && <p>Loading…</p>}
{error && <p style={{ color: 'red' }}>{error.message}</p>}
{loadInfo && (
<p>{loadInfo.width} × {loadInfo.height} px · {loadInfo.bands} band(s)</p>
)}
<button onClick={zoomIn}>+</button>
<button onClick={zoomOut}>-</button>
<button onClick={zoomToFit}>Fit</button>
<button onClick={() => rotate(90)}>↻</button>
</div>
);
}Custom dimensions and theme
<TiffViewer
url="https://example.com/image.tiff"
height="80vh"
width={1200}
theme="light"
initialZoom="original"
/>Styling
The root element has class rtv-container plus either rtv-container--dark or rtv-container--light. Override in your CSS:
.rtv-container {
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
/* Move toolbar to bottom-left */
.rtv-toolbar {
top: auto;
bottom: 16px;
right: auto;
left: 16px;
}Available CSS class hooks:
| Class | Element |
|-------|---------|
| .rtv-container | Root wrapper div |
| .rtv-container--dark | Applied when theme="dark" |
| .rtv-container--light | Applied when theme="light" |
| .rtv-map | OpenLayers map div |
| .rtv-toolbar | Floating controls wrapper |
| .rtv-loading-overlay | Loading spinner overlay |
| .rtv-error-overlay | Error message overlay |
Pre-converting to COG
For the best experience with large files, convert your TIFFs to Cloud-Optimized GeoTIFF (COG) before serving. The component works with both — but COG files stream only the tiles in view rather than downloading the whole file.
# Using rio-cogeo (recommended)
pip install rio-cogeo
rio cogeo create input.tiff output.cog.tiff --cog-profile deflate
# Using GDAL
gdal_translate input.tiff output.cog.tiff \
-of COG \
-co COMPRESS=DEFLATE \
-co BLOCKSIZE=512 \
-co OVERVIEWS=AUTOServe the resulting COG from any host that supports HTTP range requests — S3, GCS, Cloudflare R2, nginx, Caddy, or any CDN.
If you need server-side on-the-fly conversion, a standalone FastAPI server is included in python/. See python/README.md.
Development
git clone https://github.com/codewithaashir/react-tiff-viewer.git
cd react-tiff-viewer
npm install
npm run dev # example app at http://localhost:3000
npm run typecheck # TypeScript strict check
npm run build # produce dist/See docs/CONTRIBUTING.md for full contributor guide.
Publishing to npm
npm run build
npm pack --dry-run # verify what ships
npm login
npm publish # or: npm publish --access publicLicense
MIT © Aashir Shahid
Author
Aashir Shahid
- Email: [email protected]
- GitHub: github.com/codewithaashir
- LinkedIn: linkedin.com/in/aashir-shahid-516229139
Built With
- OpenLayers 9 — WebGL tile rendering and map controls
- geotiff.js — TIFF parsing and HTTP range request streaming
- Vite 5 — library build tooling
