uix-image-magnify 0.2.1 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,695 @@
1
+ # uix-image-magnify
2
+
3
+ **Fast, accessible, SSR-safe image magnifier and product gallery for React & Next.js.**
4
+
5
+ A lightweight, fully-typed React component set for zooming images with a lens, a side-by-side pane, inner pan-and-zoom, or a follow-style magnifier. Ships with a product-style gallery (thumbnails + arrows + counter), headless hooks, and SSR-safe variants that prevent hydration mismatches in Next.js App Router, Remix, and any other SSR framework.
6
+
7
+ ---
8
+
9
+ ## Table of Contents
10
+
11
+ - [Features](#features)
12
+ - [Installation](#installation)
13
+ - [Quick Start](#quick-start)
14
+ - [Single Image Magnifier](#single-image-magnifier)
15
+ - [Zoom modes](#zoom-modes)
16
+ - [Zoom triggers](#zoom-triggers)
17
+ - [Inner mode panning](#inner-mode-panning)
18
+ - [Wheel zoom](#wheel-zoom)
19
+ - [Responsive behaviour](#responsive-behaviour)
20
+ - [Product Gallery](#product-gallery)
21
+ - [Thumbnails](#thumbnails)
22
+ - [Controlled gallery](#controlled-gallery)
23
+ - [Custom arrows](#custom-arrows)
24
+ - [Disable features](#disable-features)
25
+ - [Side-by-side pane inside the gallery](#side-by-side-pane-inside-the-gallery)
26
+ - [SSR & Next.js](#ssr--nextjs)
27
+ - [Hooks](#hooks)
28
+ - [useImageSlider](#useimageslider)
29
+ - [useImageMagnify](#useimagemagnify)
30
+ - [Props Reference](#props-reference)
31
+ - [ImageMagnifyProps](#imagemagnifyprops)
32
+ - [ImageMagnifyGalleryProps](#imagemagnifygalleryprops)
33
+ - [Styling](#styling)
34
+ - [Accessibility](#accessibility)
35
+ - [TypeScript](#typescript)
36
+ - [Package Exports](#package-exports)
37
+ - [Browser Support](#browser-support)
38
+ - [License](#license)
39
+
40
+ ---
41
+
42
+ ## Features
43
+
44
+ - 🔍 **4 zoom modes** — `lens`, `side-by-side`, `inner`, `follow`
45
+ - 🖱️ **6 zoom triggers** — `hover`, `click`, `double-click`, `hold`, `wheel`, `none`
46
+ - 🖼️ **Product gallery** with thumbnails, arrows, counter, and loop
47
+ - ♿ **Accessible** — keyboard navigation, ARIA roles, live region, `aria-current` thumbnails
48
+ - 🧠 **SSR-safe** — dedicated `/ssr` entry point eliminates hydration mismatches
49
+ - 📱 **Responsive** — zoom pane automatically moves below the image on mobile
50
+ - 🎨 **Customizable** — every part is styleable; arrows accept custom React nodes
51
+ - 🪝 **Headless hooks** — `useImageMagnify`, `useImageSlider`
52
+ - 🧩 **TypeScript-first** with strict prop types
53
+ - 🌲 **Tree-shakeable** — `sideEffects: false`, ESM + CJS builds
54
+ - ⚡ **Performance** — `requestAnimationFrame`-batched DOM writes; zero re-renders during zoom
55
+
56
+ ---
57
+
58
+ ## Installation
59
+
60
+ ```bash
61
+ npm install uix-image-magnify
62
+ # or
63
+ yarn add uix-image-magnify
64
+ # or
65
+ pnpm add uix-image-magnify
66
+ ```
67
+
68
+ `react` and `react-dom` (`>=17`) are peer dependencies. `swiper` is installed automatically as a dependency for the gallery.
69
+
70
+ **Requirements:** Node 18+ for the build toolchain (not for runtime). Modern evergreen browsers (see [Browser Support](#browser-support)).
71
+
72
+ ---
73
+
74
+ ## Quick Start
75
+
76
+ ```tsx
77
+ import { ImageMagnify } from "uix-image-magnify";
78
+
79
+ export default function ProductImage() {
80
+ return (
81
+ <ImageMagnify
82
+ src="/images/shoe.jpg"
83
+ zoomSrc="/images/shoe-large.jpg"
84
+ alt="Red running shoe"
85
+ width={500}
86
+ zoomScale={2.5}
87
+ />
88
+ );
89
+ }
90
+ ```
91
+
92
+ ---
93
+
94
+ ## Single Image Magnifier
95
+
96
+ `ImageMagnify` renders an image and zooms it on interaction. The high-res `zoomSrc` is used for the zoomed view while the regular `src` is shown normally — perfect for product photos.
97
+
98
+ ```tsx
99
+ import { ImageMagnify } from "uix-image-magnify";
100
+
101
+ <ImageMagnify
102
+ src="/thumb.jpg"
103
+ zoomSrc="/large.jpg"
104
+ alt="Product"
105
+ width="100%"
106
+ mode="side-by-side"
107
+ zoomType="hover"
108
+ zoomScale={3}
109
+ panePosition="right"
110
+ paneWidth={420}
111
+ paneHeight={420}
112
+ lensShape="circle"
113
+ lensSize={160}
114
+ lazy
115
+ />;
116
+ ```
117
+
118
+ ### Zoom modes
119
+
120
+ | Mode | Description |
121
+ | -------------- | ------------------------------------------------------------- |
122
+ | `lens` | A circular/square lens follows the cursor over the image. |
123
+ | `side-by-side` | A fixed pane appears beside the image (classic Amazon style). |
124
+ | `inner` | The image itself scales and pans inside its own container. |
125
+ | `follow` | A lens follows the cursor with an optional offset. |
126
+
127
+ ### Zoom triggers
128
+
129
+ | Trigger | Behavior |
130
+ | -------------- | --------------------------------------------------------- |
131
+ | `hover` | Zooms while the pointer is over the image. |
132
+ | `click` | Toggles zoom on click. |
133
+ | `double-click` | Toggles zoom on double-click. |
134
+ | `hold` | Zooms only while the pointer is pressed down. |
135
+ | `wheel` | Scroll to zoom; supports `minZoomScale` / `maxZoomScale`. |
136
+ | `none` | Disables interaction (renders a plain image). |
137
+
138
+ > **Touch devices:** interaction behaves as tap-to-toggle regardless of `zoomType` (except `none`).
139
+ >
140
+ > **Wheel mode:** on desktop, zoom is driven by the wheel event only — a plain click does not toggle zoom.
141
+
142
+ ### Inner mode panning
143
+
144
+ When `mode="inner"`, set `dragMode`:
145
+
146
+ - `dragMode="follow"` (default) — the pixel under the cursor stays under the cursor.
147
+ - `dragMode="pan"` — drag to pan around the zoomed image. The image is **centered** the first time you zoom in.
148
+
149
+ ```tsx
150
+ <ImageMagnify
151
+ src="/product.jpg"
152
+ alt="Product"
153
+ mode="inner"
154
+ dragMode="pan"
155
+ zoomType="click"
156
+ zoomScale={3}
157
+ />
158
+ ```
159
+
160
+ ### Wheel zoom
161
+
162
+ ```tsx
163
+ <ImageMagnify
164
+ src="/product.jpg"
165
+ alt="Product"
166
+ zoomType="wheel"
167
+ zoomScale={2}
168
+ minZoomScale={1.5}
169
+ maxZoomScale={6}
170
+ onZoomChange={(s) => console.log("scale:", s)}
171
+ />
172
+ ```
173
+
174
+ ### Responsive behaviour
175
+
176
+ - On screens `≤ 768px` wide, `width` is forced to `"100%"`.
177
+ - When `mode="side-by-side"` and the pane would render to the left/right on mobile, it automatically flows **below** the image instead — no overflow, no clipping.
178
+ - The `useMediaQuery` hook is SSR-safe: it returns `false` during SSR and the first client render, then updates before paint.
179
+
180
+ ---
181
+
182
+ ## Product Gallery
183
+
184
+ `ImageMagnifyGallery` combines a Swiper-powered main slider with the magnifier and a lightweight thumbnail strip. It supports thumbnails, arrows, a counter, loop, keyboard navigation, and adjacent-image preloading.
185
+
186
+ ```tsx
187
+ import { ImageMagnifyGallery } from "uix-image-magnify";
188
+
189
+ const images = [
190
+ "/images/shoe-1.jpg",
191
+ "/images/shoe-2.jpg",
192
+ "/images/shoe-3.jpg",
193
+ ];
194
+
195
+ <ImageMagnifyGallery
196
+ images={images}
197
+ alt="Running shoe"
198
+ mode="side-by-side"
199
+ zoomType="hover"
200
+ zoomScale={2.5}
201
+ showArrows
202
+ showThumbnails
203
+ showCounter
204
+ thumbnailSize={72}
205
+ thumbnailGap={10}
206
+ onIndexChange={(i) => console.log("active:", i)}
207
+ />;
208
+ ```
209
+
210
+ ### Thumbnails
211
+
212
+ Thumbnails are rendered as a **plain scrollable flex strip** of `<button>` elements — not a nested Swiper. This is significantly faster (no second carousel instance, no duplicated magnifier instances) and gives you full control over styling.
213
+
214
+ - `thumbnailSize` — thumbnail width/height in px (default `80`).
215
+ - `thumbnailGap` — gap between thumbnails in px (default `8`).
216
+ - Clicking a thumbnail navigates the main slider.
217
+ - The active thumbnail receives `aria-current="true"` and merges `thumbnailActiveStyle` over `thumbnailStyle`.
218
+
219
+ ```tsx
220
+ <ImageMagnifyGallery
221
+ images={images}
222
+ alt="Product"
223
+ thumbnailSize={64}
224
+ thumbnailGap={12}
225
+ thumbnailContainerStyle={{ justifyContent: "center" }}
226
+ thumbnailStyle={{ borderRadius: 8 }}
227
+ thumbnailActiveStyle={{ borderColor: "#0af" }}
228
+ />
229
+ ```
230
+
231
+ ### Controlled gallery
232
+
233
+ ```tsx
234
+ const [active, setActive] = useState(0);
235
+
236
+ <ImageMagnifyGallery
237
+ images={images}
238
+ alt="Product"
239
+ activeIndex={active}
240
+ onIndexChange={setActive}
241
+ />;
242
+ ```
243
+
244
+ When `activeIndex` is provided, the gallery becomes **controlled**: `onIndexChange` fires on every slide change, and external updates to `activeIndex` call `swiper.slideTo(...)`.
245
+
246
+ ### Custom arrows
247
+
248
+ ```tsx
249
+ <ImageMagnifyGallery
250
+ images={images}
251
+ alt="Product"
252
+ prevArrow={<span>←</span>}
253
+ nextArrow={<span>→</span>}
254
+ arrowClassName="my-arrow"
255
+ arrowStyle={{ background: "#000", color: "#fff" }}
256
+ />
257
+ ```
258
+
259
+ Arrow buttons are automatically disabled (`disabled` + `aria-disabled`) at the ends when `loop={false}`.
260
+
261
+ ### Disable features
262
+
263
+ ```tsx
264
+ <ImageMagnifyGallery
265
+ images={images}
266
+ alt="Product"
267
+ showArrows={false}
268
+ showThumbnails={false}
269
+ showCounter={false}
270
+ enableKeyboard={false}
271
+ preloadAdjacent={false}
272
+ loop={false}
273
+ />
274
+ ```
275
+
276
+ ### Side-by-side pane inside the gallery
277
+
278
+ Swiper applies `overflow: hidden` to its wrapper, so a `side-by-side` pane positioned `left` or `right` of the image will be **clipped** by the slide boundary. Options:
279
+
280
+ 1. Use `mode="lens"`, `mode="inner"`, or `mode="follow"` — these render inside the image bounds.
281
+ 2. Use `mode="side-by-side"` with `panePosition="bottom"` and give the slide enough vertical space.
282
+ 3. Pass `swiperStyle={{ overflow: "visible" }}` to let the pane escape the slide — note that adjacent slides will then be visible too.
283
+
284
+ ```tsx
285
+ // Option 1: use lens mode
286
+ <ImageMagnifyGallery images={images} alt="Product" mode="lens" />
287
+
288
+ // Option 2: bottom pane
289
+ <ImageMagnifyGallery
290
+ images={images}
291
+ alt="Product"
292
+ mode="side-by-side"
293
+ panePosition="bottom"
294
+ paneHeight={320}
295
+ />
296
+
297
+ // Option 3: opt out of clipping
298
+ <ImageMagnifyGallery
299
+ images={images}
300
+ alt="Product"
301
+ mode="side-by-side"
302
+ panePosition="right"
303
+ swiperStyle={{ overflow: "visible" }}
304
+ />
305
+ ```
306
+
307
+ ---
308
+
309
+ ## SSR & Next.js
310
+
311
+ The magnifier touches `window`, `matchMedia`, `ResizeObserver`, and pointer events. To avoid hydration mismatches, this package ships **SSR-safe wrappers** under the `uix-image-magnify/ssr` entry point:
312
+
313
+ - `ImageMagnifySSR` — plain `<img>` on server + first client render, swaps to `<ImageMagnify>` after hydration.
314
+ - `ImageMagnifyGallerySSR` — same approach for the gallery.
315
+
316
+ Both non-SSR components are also re-exported from this entry point for convenience.
317
+
318
+ ### Option 1 — Use the SSR wrappers (recommended)
319
+
320
+ ```tsx
321
+ // app/products/[id]/page.tsx (Server Component-safe import)
322
+ import { ImageMagnifySSR } from "uix-image-magnify/ssr";
323
+
324
+ export default function ProductImage({
325
+ src,
326
+ alt,
327
+ }: {
328
+ src: string;
329
+ alt: string;
330
+ }) {
331
+ return (
332
+ <ImageMagnifySSR
333
+ src={src}
334
+ alt={alt}
335
+ width={500}
336
+ mode="side-by-side"
337
+ zoomType="hover"
338
+ />
339
+ );
340
+ }
341
+ ```
342
+
343
+ ```tsx
344
+ import { ImageMagnifyGallerySSR } from "uix-image-magnify/ssr";
345
+
346
+ <ImageMagnifyGallerySSR images={images} alt="Product" />;
347
+ ```
348
+
349
+ ### Option 2 — `next/dynamic` with `ssr: false`
350
+
351
+ ```tsx
352
+ import dynamic from "next/dynamic";
353
+
354
+ const ImageMagnify = dynamic(
355
+ () => import("uix-image-magnify").then((m) => m.ImageMagnify),
356
+ { ssr: false },
357
+ );
358
+ ```
359
+
360
+ Both approaches produce zero hydration warnings. Option 1 keeps the SSR-rendered `<img>` visible before hydration so users see the image immediately (no layout shift, no blank frame).
361
+
362
+ ### Remix / React Router
363
+
364
+ `uix-image-magnify/ssr` works anywhere `useEffect` runs. No additional config required.
365
+
366
+ ---
367
+
368
+ ## Hooks
369
+
370
+ ### `useImageSlider`
371
+
372
+ A headless, SSR-safe slider state hook. No DOM, no browser globals.
373
+
374
+ ```tsx
375
+ import { useImageSlider } from "uix-image-magnify";
376
+
377
+ const { index, setIndex, prev, next, canPrev, canNext } = useImageSlider({
378
+ length: images.length,
379
+ defaultIndex: 0,
380
+ loop: true,
381
+ onIndexChange: (i) => console.log(i),
382
+ });
383
+ ```
384
+
385
+ **Options**
386
+
387
+ | Option | Type | Default | Description |
388
+ | --------------- | ------------------------- | ------- | ----------------------------- |
389
+ | `length` | `number` | — | Total items. |
390
+ | `defaultIndex` | `number` | `0` | Initial index (uncontrolled). |
391
+ | `index` | `number` | — | Controlled index. |
392
+ | `onIndexChange` | `(index: number) => void` | — | Fired when the index changes. |
393
+ | `loop` | `boolean` | `true` | Wrap around at the ends. |
394
+
395
+ **Returns**
396
+
397
+ | Field | Type | Description |
398
+ | ---------- | ------------------------ | ----------------------------------------------------------- |
399
+ | `index` | `number` | Current (clamped) index. |
400
+ | `canPrev` | `boolean` | Whether a previous item exists (always `true` when `loop`). |
401
+ | `canNext` | `boolean` | Whether a next item exists (always `true` when `loop`). |
402
+ | `setIndex` | `(next: number) => void` | Go to a specific index (wraps when `loop`). |
403
+ | `prev` | `() => void` | Go to previous item. |
404
+ | `next` | `() => void` | Go to next item. |
405
+
406
+ `defaultIndex` is clamped to `[0, length - 1]`.
407
+
408
+ ### `useImageMagnify`
409
+
410
+ Use the same engine behind `<ImageMagnify>` directly if you need a custom renderer. Returns refs + event handlers.
411
+
412
+ ```tsx
413
+ import { useImageMagnify } from "uix-image-magnify";
414
+
415
+ const {
416
+ isZoomed,
417
+ zoomScale,
418
+ bgImage,
419
+ containerRef,
420
+ imageRef,
421
+ lensRef,
422
+ paneRef,
423
+ cursorRef,
424
+ handlers,
425
+ } = useImageMagnify({
426
+ src: "/thumb.jpg",
427
+ zoomSrc: "/large.jpg",
428
+ mode: "side-by-side",
429
+ zoomType: "hover",
430
+ zoomScale: 2.5,
431
+ });
432
+
433
+ return (
434
+ <div ref={containerRef} {...handlers} style={{ position: "relative" }}>
435
+ <img
436
+ ref={imageRef}
437
+ src="/thumb.jpg"
438
+ alt=""
439
+ style={{ display: "block", width: "100%" }}
440
+ />
441
+ {isZoomed && (
442
+ <div
443
+ ref={paneRef}
444
+ style={{
445
+ position: "absolute",
446
+ left: "100%",
447
+ top: 0,
448
+ width: 400,
449
+ height: 400,
450
+ backgroundImage: bgImage ? `url("${bgImage}")` : undefined,
451
+ backgroundRepeat: "no-repeat",
452
+ }}
453
+ />
454
+ )}
455
+ </div>
456
+ );
457
+ ```
458
+
459
+ **Return value**
460
+
461
+ | Field | Type | Description |
462
+ | -------------- | ----------------------------- | ----------------------------------------------- |
463
+ | `isZoomed` | `boolean` | Whether zoom is currently active. |
464
+ | `zoomScale` | `number` | Current zoom scale. |
465
+ | `bgImage` | `string \| undefined` | URL to use as the zoomed background. |
466
+ | `containerRef` | `RefObject<HTMLDivElement>` | Attach to the wrapper element. |
467
+ | `imageRef` | `RefObject<HTMLImageElement>` | Attach to the `<img>`. |
468
+ | `lensRef` | `RefObject<HTMLDivElement>` | Attach to the lens element (lens/follow modes). |
469
+ | `paneRef` | `RefObject<HTMLDivElement>` | Attach to the side-by-side pane. |
470
+ | `cursorRef` | `RefObject<HTMLDivElement>` | Attach to the side-by-side cursor dot. |
471
+ | `handlers` | `object` | Spread on the container element. |
472
+
473
+ **Option highlights**
474
+
475
+ | Option | Type | Default |
476
+ | -------------- | --------------------------------------------------------------------- | ---------------- |
477
+ | `src` | `string` | — |
478
+ | `zoomSrc` | `string` | `src` |
479
+ | `zoomScale` | `number` | `2` |
480
+ | `minZoomScale` | `number` | `1.5` |
481
+ | `maxZoomScale` | `number` | `5` |
482
+ | `zoomType` | `"hover" \| "click" \| "double-click" \| "hold" \| "wheel" \| "none"` | `"hover"` |
483
+ | `mode` | `"lens" \| "side-by-side" \| "inner" \| "follow"` | `"side-by-side"` |
484
+ | `dragMode` | `"follow" \| "pan"` | `"follow"` |
485
+ | `panePosition` | `"left" \| "right" \| "top" \| "bottom"` | `"right"` |
486
+ | `lensSize` | `number` | `150` |
487
+ | `lensShape` | `"circle" \| "square"` | `"circle"` |
488
+ | `followOffset` | `number` | `0` |
489
+ | `zoomPreload` | `boolean` | `true` |
490
+ | `disabled` | `boolean` | `false` |
491
+ | `resetOnLeave` | `boolean` | `false` |
492
+ | `onZoomStart` | `() => void` | — |
493
+ | `onZoomEnd` | `() => void` | — |
494
+ | `onZoomChange` | `(scale: number) => void` | — |
495
+
496
+ ---
497
+
498
+ ## Props Reference
499
+
500
+ ### `ImageMagnifyProps`
501
+
502
+ Extends `UseImageMagnifyOptions` **plus**:
503
+
504
+ | Prop | Type | Default | Description |
505
+ | ---------------- | ------------------------------ | ------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
506
+ | `src` | `string` | — | Regular image URL. |
507
+ | `alt` | `string` | — | Accessible label. |
508
+ | `width` | `number \| string` | — | Outer width. On mobile, forced to `100%`. |
509
+ | `className` | `string` | — | Outer wrapper class. |
510
+ | `style` | `CSSProperties` | — | Outer wrapper style. |
511
+ | `imageClassName` | `string` | — | `<img>` class. |
512
+ | `imageStyle` | `CSSProperties` | — | `<img>` style. |
513
+ | `lensClassName` | `string` | — | Lens element class. |
514
+ | `lensStyle` | `CSSProperties` | — | Lens element style. |
515
+ | `paneClassName` | `string` | — | Side-by-side pane class. |
516
+ | `paneStyle` | `CSSProperties` | — | Side-by-side pane style. |
517
+ | `paneWidth` | `number \| string` | `400` | Pane width. |
518
+ | `paneHeight` | `number \| string` | `400` | Pane height. |
519
+ | `lazy` | `boolean` | — | Use `loading="lazy"` on the `<img>`. |
520
+ | `imageProps` | `ImgHTMLAttributes` | — | Extra props forwarded to the `<img>`. Excludes `src`, `alt`, `style`, `className`, `onLoad`, `onError` (use the dedicated props instead). |
521
+ | `onLoad` | `ImgHTMLAttributes["onLoad"]` | — | Fired on image load. |
522
+ | `onError` | `ImgHTMLAttributes["onError"]` | — | Fired on image error. |
523
+
524
+ ### `ImageMagnifyGalleryProps`
525
+
526
+ Extends `ImageMagnifyProps` **minus** `src` / `onLoad` / `onError`, plus:
527
+
528
+ | Prop | Type | Default | Description |
529
+ | ----------------------------- | ------------------------- | ------- | --------------------------------------------- |
530
+ | `images` | `string[]` | — | Image URLs. |
531
+ | `alt` | `string` | — | Alt text (shared; per-slide alt is appended). |
532
+ | `activeIndex` | `number` | — | Controlled index. |
533
+ | `defaultIndex` | `number` | `0` | Initial index (uncontrolled). |
534
+ | `onIndexChange` | `(index: number) => void` | — | Fired when the active slide changes. |
535
+ | `showArrows` | `boolean` | `true` | Prev/next arrows. |
536
+ | `showThumbnails` | `boolean` | `true` | Thumbnail strip. |
537
+ | `showCounter` | `boolean` | `false` | "1 / N" counter overlay. |
538
+ | `loop` | `boolean` | `true` | Infinite loop. |
539
+ | `enableKeyboard` | `boolean` | `true` | Arrow-key navigation. |
540
+ | `preloadAdjacent` | `boolean` | `true` | Preload next/prev images (respects `loop`). |
541
+ | `viewportClassName` | `string` | — | Main carousel viewport class. |
542
+ | `viewportStyle` | `CSSProperties` | — | Main carousel viewport style. |
543
+ | `swiperStyle` | `CSSProperties` | — | Style applied to the inner Swiper element. |
544
+ | `thumbnailContainerClassName` | `string` | — | Thumbnail strip class. |
545
+ | `thumbnailContainerStyle` | `CSSProperties` | — | Thumbnail strip style. |
546
+ | `thumbnailClassName` | `string` | — | Thumbnail button class. |
547
+ | `thumbnailActiveClassName` | `string` | — | Added to the active thumbnail button. |
548
+ | `thumbnailStyle` | `CSSProperties` | — | Thumbnail button style. |
549
+ | `thumbnailActiveStyle` | `CSSProperties` | — | Merged over `thumbnailStyle` when active. |
550
+ | `arrowClassName` | `string` | — | Arrow button class. |
551
+ | `arrowStyle` | `CSSProperties` | — | Arrow button style. |
552
+ | `thumbnailSize` | `number` | `80` | Thumbnail width/height (px). |
553
+ | `thumbnailGap` | `number` | `8` | Thumbnail gap (px). |
554
+ | `prevArrow` | `ReactNode` | — | Custom previous arrow. |
555
+ | `nextArrow` | `ReactNode` | — | Custom next arrow. |
556
+
557
+ ---
558
+
559
+ ## Styling
560
+
561
+ Every part of the component is styleable via `className` / `style` props. The magnifier uses minimal inline styles and does not require any CSS import.
562
+
563
+ ```tsx
564
+ <ImageMagnify
565
+ src="/a.jpg"
566
+ alt="Product"
567
+ width={480}
568
+ className="magnify-root"
569
+ imageClassName="magnify-image"
570
+ lensClassName="magnify-lens"
571
+ lensStyle={{ borderColor: "#111", boxShadow: "0 6px 20px rgba(0,0,0,.2)" }}
572
+ paneClassName="magnify-pane"
573
+ paneStyle={{ borderRadius: 12, border: "1px solid #eee" }}
574
+ />
575
+ ```
576
+
577
+ ```css
578
+ .magnify-root {
579
+ border-radius: 12px;
580
+ overflow: hidden;
581
+ background: #f7f7f7;
582
+ }
583
+
584
+ .magnify-lens {
585
+ border-color: #111;
586
+ }
587
+
588
+ .magnify-pane {
589
+ background-color: #fafafa;
590
+ }
591
+ ```
592
+
593
+ The gallery ships with inline defaults for arrows and thumbnails that you can override fully:
594
+
595
+ ```tsx
596
+ <ImageMagnifyGallery
597
+ images={images}
598
+ alt="Product"
599
+ thumbnailSize={64}
600
+ thumbnailGap={12}
601
+ thumbnailContainerStyle={{ justifyContent: "center" }}
602
+ thumbnailStyle={{ borderRadius: 8 }}
603
+ thumbnailActiveStyle={{ borderColor: "#0af" }}
604
+ arrowStyle={{ background: "#111", color: "#fff" }}
605
+ />
606
+ ```
607
+
608
+ Because the gallery does not import any CSS from `swiper/css`, only the base Swiper runtime is used (no default arrow/thumbnail styles leak in). All visuals come from inline styles + your classes.
609
+
610
+ ---
611
+
612
+ ## Accessibility
613
+
614
+ - Root gallery uses `role="region"`, `aria-roledescription="carousel"`, `aria-label`.
615
+ - Each image container is `role="img"` with `aria-label={alt}`. It is focusable (`tabIndex={0}`) **only when interaction is enabled** (`zoomType !== "none"` and not `disabled`).
616
+ - Hidden `aria-live="polite"` region announces **"Image X of Y"** on slide change.
617
+ - Thumbnails are `<button>` elements with `aria-label` and `aria-current="true"` for the active one; the strip is a `role="group"` labelled "Image thumbnails".
618
+ - Arrow buttons get `aria-disabled` and `disabled` at the ends when `loop={false}`.
619
+ - Disabled state exposes `aria-disabled`.
620
+ - Swiper's `A11y` module is enabled for the main slider.
621
+ - Custom arrows accept any `ReactNode`; default icons are `<svg aria-hidden="true">` with `aria-label` on the buttons.
622
+
623
+ ### Keyboard controls
624
+
625
+ | Key | Action |
626
+ | ----------------- | ------------------------------------------------- |
627
+ | **← / → / ↑ / ↓** | Move the zoom pointer (image container). |
628
+ | **+ / = / -** | Zoom in/out (when `zoomType="wheel"`). |
629
+ | **Enter / Space** | Toggle zoom. |
630
+ | **Esc** | Reset zoom. |
631
+ | **← / →** | Navigate slides (gallery, when `enableKeyboard`). |
632
+
633
+ ---
634
+
635
+ ## TypeScript
636
+
637
+ The package is written in strict TypeScript and exports all prop and option types:
638
+
639
+ ```ts
640
+ import type {
641
+ ImageMagnifyProps,
642
+ ImageMagnifyGalleryProps,
643
+ UseImageMagnifyOptions,
644
+ UseImageSliderOptions,
645
+ UseImageSliderReturn,
646
+ ZoomType,
647
+ PanePosition,
648
+ LensShape,
649
+ MagnifyMode,
650
+ DragMode,
651
+ } from "uix-image-magnify";
652
+ ```
653
+
654
+ ---
655
+
656
+ ## Package Exports
657
+
658
+ | Import path | Contents |
659
+ | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
660
+ | `uix-image-magnify` | `ImageMagnify`, `ImageMagnifyGallery`, `useImageMagnify`, `useImageSlider` + all types |
661
+ | `uix-image-magnify/ssr` | `ImageMagnifySSR`, `ImageMagnifyGallerySSR`, plus non-SSR re-exports (`ImageMagnify`, `ImageMagnifyGallery`) and their types |
662
+
663
+ Both entry points are dual-published (ESM + CJS) with bundled TypeScript declarations.
664
+
665
+ ---
666
+
667
+ ## Browser Support
668
+
669
+ Modern evergreen browsers (Chrome, Edge, Firefox, Safari). Requires:
670
+
671
+ - `ResizeObserver`
672
+ - `matchMedia`
673
+ - Pointer Events
674
+ - CSS `transform` / `background-position`
675
+
676
+ SSR wrappers render plain `<img>` on the server for maximum compatibility. The library uses React `>=17` and has no other runtime peer dependencies besides `swiper`.
677
+
678
+ ---
679
+
680
+ ## License
681
+
682
+ MIT © uix-image-magnify contributors
683
+
684
+ ---
685
+
686
+ ## Contributing
687
+
688
+ Issues and PRs are welcome. Before submitting, please run:
689
+
690
+ ```bash
691
+ npm run typecheck
692
+ npm run build
693
+ ```
694
+
695
+ Both must pass cleanly.