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 +695 -0
- package/dist/{ImageMagnify-DssaU7jS.d.cts → ImageMagnifyGallery-Bw_SkDZq.d.cts} +35 -18
- package/dist/{ImageMagnify-DssaU7jS.d.ts → ImageMagnifyGallery-Bw_SkDZq.d.ts} +35 -18
- package/dist/index.cjs +128 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -50
- package/dist/index.d.ts +3 -50
- package/dist/index.js +130 -75
- package/dist/index.js.map +1 -1
- package/dist/ssr/index.cjs +406 -4
- package/dist/ssr/index.cjs.map +1 -1
- package/dist/ssr/index.d.cts +11 -4
- package/dist/ssr/index.d.ts +11 -4
- package/dist/ssr/index.js +407 -7
- package/dist/ssr/index.js.map +1 -1
- package/package.json +1 -1
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.
|