Skip to main content

Zoom Image

Zoom Image is a media component for enlarging images in place, like inspecting a product photo or a screenshot. People click or tap the image to open an enlarged view and can browse related images in a group.

Import

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

Other links

<ZoomImage
  src="https://cdn.vortex.design/docs/components/image/melbourne.jpg"
  alt="Flinders Street Station in Melbourne"
  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://cdn.vortex.design/docs/components/image/melbourne.jpg"
    alt="Flinders Street Station, square crop"
    width={{ xs: "8rem", md: "12rem" }}
    aspectRatio={1}
    objectFit="cover"
    borderRadius="md"
    bordered
  />
  <ZoomImage
    src="https://cdn.vortex.design/docs/components/image/melbourne.jpg"
    alt="Flinders Street Station, full 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://cdn.vortex.design/docs/components/image/melbourne.jpg"
  alt="Flinders Street Station preview"
  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://cdn.vortex.design/docs/components/image/melbourne.jpg"
  alt="Flinders Street Station with a generous zoom margin"
  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 { useRef, useState } from "react";

export const ZoomImageControls = () => {
  const ref = useRef<ZoomImageRef>(null);
  const [zoomed, setZoomed] = useState(false);
  return (
    <Stack gap={3}>
      <ZoomImage
        ref={ref}
        src="https://cdn.vortex.design/docs/components/image/melbourne.jpg"
        alt="Flinders Street Station panorama"
        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://cdn.vortex.design/docs/components/image/melbourne.jpg"
  alt="Flinders Street Station with custom image rendering"
  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="Station photo that failed to load"
  width="16rem"
  maxWidth="100%"
  aspectRatio={16 / 9}
/>

Composition

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>
      <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 { useRef, useState } from "react";

// photos is the array from the gallery example above.
export const ZoomImageGroupControls = () => {
  const ref = useRef<ZoomImageGroupRef>(null);
  const [index, setIndex] = useState<number | null>(null);
  return (
    <Stack gap={3}>
      <ZoomImage.Group ref={ref} onIndexChange={setIndex}>
        <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
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.
TabShiftTab
Focus stays inside the enlarged view while it is open.
Provide alt for every image; it names both the zoom button and the enlarged view.