Skip to main content

Zoom Image

Zoom Image is a media element that enlarges an image in place, such as inspecting a product photo or a screenshot.

Import

import { ZoomImage } from '@vortexlabs/vortex';
import type { ZoomImageProps } from '@vortexlabs/vortex';

Allied components

Other links

<ZoomImage
  src={"https://images.unsplash.com/photo-1501854140801-50d01698950b?q=80&w=1600&auto=format&fit=crop"}
  alt="Green mountain valley"
  closeAriaLabel="Close image"
  width="300px"
  aspectRatio={1.5}
/>

Features

  • Browses related images with ZoomImage.Group.
  • Supports responsive sizes, cropping, and borders.
  • Keeps a margin between the enlarged image and the viewport.
  • Reports zoom changes and opens or closes through a ref.
  • Renders a custom image element with renderImageSlot.

Size and Object Fit

Set the size with width, height, maxWidth, or aspectRatio; each accepts a breakpoint object. objectFit controls the crop, and borderRadius and bordered style the image.

import { Stack, ZoomImage } from "@vortexlabs/vortex";

<Stack direction="row" wrap gap={4}>
  <ZoomImage
    src="https://images.unsplash.com/photo-1501854140801-50d01698950b?q=80&w=1600&auto=format&fit=crop"
    alt="Mountain valley, square crop"
    closeAriaLabel="Close image"
    width={{ xs: "8rem", md: "12rem" }}
    aspectRatio={1}
    objectFit="cover"
    borderRadius="md"
    bordered
  />
  <ZoomImage
    src="https://images.unsplash.com/photo-1501854140801-50d01698950b?q=80&w=1600&auto=format&fit=crop"
    alt="Mountain valley, full image"
    closeAriaLabel="Close image"
    width={{ xs: "8rem", md: "12rem" }}
    aspectRatio={1}
    objectFit="contain"
    borderRadius="md"
    bordered
  />
</Stack>

Disabled

Set disabled to show the image without letting it open.

<ZoomImage
  src="https://images.unsplash.com/photo-1501854140801-50d01698950b?q=80&w=1600&auto=format&fit=crop"
  alt="Mountain valley preview"
  closeAriaLabel="Close image"
  width="16rem"
  maxWidth="100%"
  aspectRatio={16 / 9}
  disabled
/>

Zoom Margin

zoomMargin sets the space between the enlarged image and the screen edge. Pass spacing units or a CSS length; the default is 6.

<ZoomImage
  src="https://images.unsplash.com/photo-1501854140801-50d01698950b?q=80&w=1600&auto=format&fit=crop"
  alt="Mountain valley with a generous zoom margin"
  closeAriaLabel="Close image"
  width="16rem"
  maxWidth="100%"
  zoomMargin="3rem"
/>

Events and Ref

onZoomChange tells you when the image opens or closes. Pass a ref to open or close it from another control, like a button.

Image is inline.
"use client";

import { Button, Stack, Text, ZoomImage } from "@vortexlabs/vortex";
import type { ZoomImageRef } from "@vortexlabs/vortex";
import React from "react";

export const ZoomImageControls = () => {
  const ref = React.useRef<ZoomImageRef>(null);
  const [zoomed, setZoomed] = React.useState(false);
  return (
    <Stack gap={3}>
      <ZoomImage
        ref={ref}
        src="https://images.unsplash.com/photo-1501854140801-50d01698950b?q=80&w=1600&auto=format&fit=crop"
        alt="Mountain valley panorama"
        closeAriaLabel="Close image"
        width="16rem"
        maxWidth="100%"
        onZoomChange={setZoomed}
      />
      <Button onClick={() => ref.current?.open()}>Enlarge panorama</Button>
      <Text>Image is {zoomed ? "enlarged" : "inline"}.</Text>
    </Stack>
  );
};

Custom Rendering

Use renderImageSlot to supply your own image element, such as a framework image component. Pass all the attributes it receives, including the ref, to that element.

<ZoomImage
  src="https://images.unsplash.com/photo-1501854140801-50d01698950b?q=80&w=1600&auto=format&fit=crop"
  alt="Mountain valley with custom image rendering"
  closeAriaLabel="Close image"
  width="16rem"
  maxWidth="100%"
  renderImageSlot={(attributes) => <img {...attributes} decoding="async" />}
/>

Fallback

If the image fails to load, ZoomImage shows a placeholder and can't be opened. Use onError to react to the failure.

<ZoomImage
  src="https://cdn.vortex.design/docs/components/image/missing.webp"
  alt="Photo that failed to load"
  closeAriaLabel="Close image"
  width="16rem"
  maxWidth="100%"
  aspectRatio={16 / 9}
/>

ZoomImage.Group

Wrap related images in ZoomImage.Group and use ZoomImage.Item for each one. People can then move between the images without closing the enlarged view.

In a grid, give each item width="100%" and a block-level button through attributes, so it fills its cell.

"use client";

import { Grid, ZoomImage } from "@vortexlabs/vortex";

const unsplash = (id: string) =>
  `https://images.unsplash.com/photo-${id}?q=80&w=1600&auto=format&fit=crop`;

const photos = [
  {
    src: unsplash("1506973035872-a4ec16b8e8d9"),
    alt: "Aerial view of a beach with turquoise water",
  },
  {
    src: unsplash("1469474968028-56623f02e42e"),
    alt: "Golden light through trees in a misty forest",
  },
  {
    src: unsplash("1501854140801-50d01698950b"),
    alt: "Lush green mountain valley",
  },
  {
    src: unsplash("1432405972618-c60b0225b8f9"),
    alt: "Waterfall cascading over mossy rocks",
  },
  {
    src: unsplash("1464802686167-b939a6910659"),
    alt: "Milky Way galaxy over dark landscape",
  },
  {
    src: unsplash("1441974231531-c6227db76b6e"),
    alt: "Sunlit forest path with tall trees",
  },
];

export const ZoomImageGallery = () => {
  return (
    <ZoomImage.Group
      closeAriaLabel="Close image"
      previousAriaLabel="Previous image"
      nextAriaLabel="Next image"
    >
      <Grid display="grid" columns={4} gap={2} width="100%" maxWidth="480px">
        {photos.map((photo, index) => {
          const wide = index % 3 === 0;
          return (
            <Grid.Item key={photo.src} columns={wide ? "span 2" : undefined}>
              <ZoomImage.Item
                src={photo.src}
                alt={photo.alt}
                width="100%"
                aspectRatio={wide ? 2 : 1}
                borderRadius="sm"
                attributes={{ style: { display: "block", width: "100%" } }}
              />
            </Grid.Item>
          );
        })}
      </Grid>
    </ZoomImage.Group>
  );
};

Group Events and Ref

onIndexChange reports the index of the open image, or null when the view closes. A ZoomImageGroupRef opens the view at an index with open(index).

No image open.
"use client";

import { Button, Stack, Text, ZoomImage } from "@vortexlabs/vortex";
import type { ZoomImageGroupRef } from "@vortexlabs/vortex";
import React from "react";

// photos is the array from the gallery example above.
export const ZoomImageGroupControls = () => {
  const ref = React.useRef<ZoomImageGroupRef>(null);
  const [index, setIndex] = React.useState<number | null>(null);
  return (
    <Stack gap={3}>
      <ZoomImage.Group
        ref={ref}
        onIndexChange={setIndex}
        closeAriaLabel="Close image"
        previousAriaLabel="Previous image"
        nextAriaLabel="Next image"
      >
        <Stack direction="row" wrap gap={2}>
          {photos.slice(0, 3).map((photo) => (
            <ZoomImage.Item
              key={photo.src}
              src={photo.src}
              alt={photo.alt}
              width="8rem"
              aspectRatio={1}
              borderRadius="sm"
            />
          ))}
        </Stack>
      </ZoomImage.Group>
      <Button onClick={() => ref.current?.open(2)}>Open the third image</Button>
      <Text>{index === null ? "No image open." : `Showing image ${index + 1}.`}</Text>
    </Stack>
  );
};

Accessibility

Key
Description
TabShiftTab
Focus stays inside the enlarged view while it is open.
EnterSpace
Pressing Enter or Space while an image is focused opens the enlarged view.
Escape
Pressing Escape closes the enlarged view and returns focus to the element that opened it, or in a group to the last image shown.
Left arrowRight arrow
In a group, ArrowLeft and ArrowRight show the previous and next image.
Provide alt for every image; it names both the zoom button and the enlarged view.
Provide closeAriaLabel on ZoomImage, and closeAriaLabel, previousAriaLabel, and nextAriaLabel on ZoomImage.Group, to name the enlarged view's buttons.