Skip to content

rpearce/react-medium-image-zoom

react-medium-image-zoom

npm version react-medium-image-zoom bundlejs badge npm downloads All Contributors

The original medium.com-inspired image zooming library for React.

View the storybook examples to see various usages.

Features:

Requirements to know about:

  • <dialog> element (caniuse dialog)
  • ResizeObserver (caniuse ResizeObserver)
  • Package build target is ES2021. If you need to support older environments, run this package through your build system.

Quickstart

npm install --save react-medium-image-zoom
import React from 'react'
import Zoom from 'react-medium-image-zoom'
import 'react-medium-image-zoom/dist/styles.css'

export const MyImg = () => (
  <Zoom>
    <img
      alt="That Wanaka Tree, New Zealand by Laura Smetsers"
      src="/path/to/thatwanakatree.jpg"
      width="500"
    />
  </Zoom>
)

API

You can pass these options to either the Uncontrolled (default) or Controlled components.

export interface UncontrolledProps {
  // Accessible label text for when you want to unzoom.
  // Default: 'Minimize image'
  a11yNameButtonUnzoom?: string

  // Accessible label text for when you want to zoom.
  // Default: 'Expand image'
  a11yNameButtonZoom?: string

  // Your image (required).
  children: ReactNode

  // Custom CSS className to add to the zoomed <dialog>.
  classDialog?: string

  // Provide your own unzoom button icon.
  // Default: ICompress
  IconUnzoom?: ElementType

  // Provide your own zoom button icon.
  // Default: IEnlarge
  IconZoom?: ElementType

  // Specify what type of element should be used for
  // internal component usage. This is useful if the
  // image is inside a <p> or <button>, for example.
  // Default: 'div'
  wrapElement?: 'div' | 'span'

  // Provide your own custom modal content component.
  ZoomContent?: (props: {
    img: ReactElement | null;
    buttonUnzoom: ReactElement<HTMLButtonElement>;
    onUnzoom: () => void;
  }) => ReactElement;

  // Higher quality image attributes to use on zoom.
  zoomImg?: ImgHTMLAttributes<HTMLImageElement>

  // Offset in pixels the zoomed image should
  // be from the window's boundaries.
  // Default: 0
  zoomMargin?: number
}

You can pass these options to only the Controlled component.

export interface ControlledProps {
  // ...same as UncontrolledProps

  // Tell the component whether or not it should be zoomed
  // Default: false
  isZoomed: boolean

  // Listen for hints from the component about when you
  // should zoom (`true` value) or unzoom (`false` value)
  onZoomChange?: (value: boolean) => void
}

Basic Usage

Uncontrolled component (default)

Import the component and the CSS, wrap your image with the component, and the component will handle it's own state.

import React from 'react'
import Zoom from 'react-medium-image-zoom'
import 'react-medium-image-zoom/dist/styles.css'

// <img />
export const MyImg = () => (
  <Zoom>
    <img
      alt="That Wanaka Tree, New Zealand by Laura Smetsers"
      src="/path/to/thatwanakatree.jpg"
      width="500"
    />
  </Zoom>
)

// <div>
export const MyDiv = () => (
  <Zoom>
    <div
      aria-label="That Wanaka Tree, New Zealand by Laura Smetsers"
      role="img"
      style={{
        backgroundColor: '#fff',
        backgroundImage: `url("/path/to/thatwanakatree.jpg")`,
        backgroundPosition: '50%',
        backgroundRepeat: 'no-repeat',
        backgroundSize: 'cover',
        height: '0',
        paddingBottom: '56%',
        width: '100%',
      }}
    />
  </Zoom>
)

// <picture>
export const MyPicture = () => (
  <Zoom>
    <picture>
      <source media="(max-width: 800px)" srcSet="/path/to/teAraiPoint.jpg" />
      <img
        alt="A beautiful, serene setting in nature"
        src="/path/to/thatwanakatree.jpg"
        width="500"
      />
    </picture>
  </Zoom>
)

// <figure>
export const MyFigure = () => (
  <figure>
    <Zoom>
      <img
        alt="That Wanaka Tree, New Zealand by Laura Smetsers"
        src="/path/to/thatwanakatree.jpg"
        width="500"
      />
    </Zoom>
    <figcaption>Photo by Laura Smetsers</figcaption>
  </figure>
)

Controlled component

Import the Controlled component and the CSS, wrap your image with the component, and then dictate the isZoomed state to the component.

import React, { useCallback, useState } from 'react'
import { Controlled as ControlledZoom } from 'react-medium-image-zoom'
import 'react-medium-image-zoom/dist/styles.css'

const MyComponent = () => {
  const [isZoomed, setIsZoomed] = useState(false)

  const handleZoomChange = useCallback(shouldZoom => {
    setIsZoomed(shouldZoom)
  }, [])

  return (
    <ControlledZoom isZoomed={isZoomed} onZoomChange={handleZoomChange}>
      <img
        alt="That wanaka tree, alone in the water near mountains"
        src="/path/to/thatwanakatree.jpg"
        width="500"
      />
    </ControlledZoom>
  )
)

export default MyComponent

The onZoomChange prop accepts a callback that will receive true or false based on events that occur (like click or scroll events) to assist you in determining when to zoom and unzoom the component.

Styles

You can import the default styles from react-medium-image-zoom/dist/styles.css and override the values from your code, or you can copy the styles.css file and alter it to your liking. The latter is the best option, given rems should be used instead of px to account for different default browser font sizes, and it's hard for a library to guess at what these values should be.

An example of customizing the transition duration, timing function, overlay background color, and unzoom button styles with :focus-visible can be found in this story: https://rpearce.github.io/react-medium-image-zoom/?path=/story/img--custom-modal-styles

Custom zoom modal content

If you want to customize the zoomed modal experience with a caption, form, or other set of components, you can do so by providing a custom component to the ZoomContent prop.

View the live example of custom zoom modal content.

Below is some example code that demonstrates how to use this feature.

export const MyImg = () => (
  <Zoom ZoomContent={CustomZoomContent}>
    <img
      alt="That Wanaka Tree, New Zealand by Laura Smetsers"
      src="/path/to/thatwanakatree.jpg"
      width="500"
    />
  </Zoom>
)

const CustomZoomContent = ({
  buttonUnzoom, // default unzoom button
  modalState,   // current state of the zoom modal: UNLOADED, LOADING, LOADED, UNLOADING
  img,          // your image, prepped for zooming
  //onUnzoom,   // unused here, but a callback to manually unzoom the image and
                //   close the modal if you want to use your own buttons or
                //   listeners in your custom experience
}) => {
  const [isLoaded, setIsLoaded] = useState(false)

  useLayoutEffect(() => {
    if (modalState === 'LOADED') {
      setIsLoaded(true)
    } else if (modalState === 'UNLOADING') {
      setIsLoaded(false)
    }
  }, [modalState])

  const classCaption = isLoaded
    ? 'zoom-caption zoom-caption--loaded'
    : 'zoom-caption'

  return <>
    {buttonUnzoom}

    <figure>
      {img}
      <figcaption className={classCaption}>
        That Wanaka Tree, also known as the Wanaka Willow, is a willow tree
        located at the southern end of Lake Wānaka in the Otago region of New
        Zealand.
        <cite className="zoom-caption-cite">
          Wikipedia, <a className="zoom-caption-link" href="https://en.wikipedia.org/wiki/That_Wanaka_Tree">
            That Wanaka Tree
          </a>
        </cite>
      </figcaption>
    </figure>
  </>
}

Migrating From v4 to v5

Here are the prop changes from v4 to be aware of:

  • closeText was renamed to a11yNameButtonUnzoom
  • openText was renamed to a11yNameButtonZoom
  • overlayBgColorStart was removed and is now controlled via the CSS selector [data-rmiz-modal-overlay="hidden"]
  • overlayBgColorEnd was removed and is now controlled via the CSS selector [data-rmiz-modal-overlay="visible"]
  • portalEl was removed, for we are using the <dialog> element now
  • transitionDuration was removed and is now controlled via the CSS selectors [data-rmiz-modal-overlay] and [data-rmiz-modal-img]
  • wrapElement was removed then added back in v5.1.0
  • wrapStyle was removed
  • zoomZindex was removed, for we are using the <dialog> element now

And you can now provide zoomImg props to specify a different image to load when zooming.

Contributors ✨

Thanks goes to these wonderful people (emoji key):

Robert Pearce
Robert Pearce

πŸ’» πŸ’¬ ⚠️ πŸ› πŸ’‘ 🎨 πŸ‘€ πŸ€” πŸ“–
Cameron Bothner
Cameron Bothner

πŸ’» πŸ“– πŸ› πŸ’‘ πŸ€” πŸ‘€ ⚠️
Jeremy Bini
Jeremy Bini

πŸ’» πŸ›
ismay
ismay

πŸ› πŸ€”
Rajit Singh
Rajit Singh

πŸ›
Roberto Saccon
Roberto Saccon

πŸ›
wtfdaemon
wtfdaemon

πŸ›
Josh Sloat
Josh Sloat

πŸ› πŸ’» πŸ’‘ πŸ‘€ πŸ€” πŸ“– 🎨 πŸ’¬
Aswin
Aswin

πŸ’¬
Alex Shelkovskiy
Alex Shelkovskiy

πŸ›
Adrian Bindiu
Adrian Bindiu

πŸ›
Kendall Buchanan
Kendall Buchanan

πŸ›
Kaycee
Kaycee

πŸ’»
Anuj
Anuj

πŸ› πŸ’¬
Ludwig Frank
Ludwig Frank

πŸ› πŸ’»
LX
LX

πŸ› πŸ€”
Rosen Tomov
Rosen Tomov

πŸ›
Tom Moor
Tom Moor

πŸ’» πŸ›
Johan Preynat
Johan Preynat

πŸ’» πŸ›
Rahul Gaba
Rahul Gaba

πŸ’» πŸ›
Spencer Davis
Spencer Davis

πŸ’» πŸ€” πŸ‘€ 🎨
dnlnvl
dnlnvl

πŸ’»
Madi
Madi

πŸ€”
Ben Hood
Ben Hood

πŸ€” πŸ› πŸ’‘ πŸ‘€
Navilan
Navilan

πŸ€”
13806
13806

πŸ›
Akshay Kadam (A2K)
Akshay Kadam (A2K)

πŸ› πŸ€”
Jake Stewart
Jake Stewart

πŸ› πŸ€”
hhh
hhh

πŸ›
@davalapar
@davalapar

πŸ›
Sun Knudsen
Sun Knudsen

πŸ’» πŸ› πŸ€” πŸ’‘ πŸ’¬ πŸ‘€ ⚠️ πŸ“–
Douglas Galdino
Douglas Galdino

πŸ’» πŸ“– πŸ› πŸ€” πŸ’‘ πŸ‘€ ⚠️
Mohammed Faragallah
Mohammed Faragallah

πŸ› πŸ€” πŸ’‘
Youngrok Kim
Youngrok Kim

πŸ’» πŸ›
Nandhagopal Ezhilmaran
Nandhagopal Ezhilmaran

πŸ›
Mattia Astorino
Mattia Astorino

πŸ›
Dan Wood
Dan Wood

πŸ“–
Zachery C Gentry
Zachery C Gentry

πŸ›
xmflsct
xmflsct

πŸ›
Will.iam
Will.iam

πŸ’» ⚠️
Gourav Goyal
Gourav Goyal

πŸ“–
Joshua Chen
Joshua Chen

πŸ› πŸ’»
David Edler
David Edler

πŸ›
rikusen0335
rikusen0335

πŸ€”
Surjith S M
Surjith S M

πŸ€”
developergunny
developergunny

πŸ›
Khan Mohsin
Khan Mohsin

πŸ’¬
Robin Goudeketting
Robin Goudeketting

πŸ›
Botros Toro
Botros Toro

πŸ€”
Christian Guevara
Christian Guevara

πŸ’¬
Johan Book
Johan Book

πŸ›
Paolo Di Bello
Paolo Di Bello

πŸ€”
Tommaso De Rossi
Tommaso De Rossi

πŸ“– πŸ›
Lezan
Lezan

πŸ› πŸ€”
Ibrahim H. Sluma
Ibrahim H. Sluma

πŸ›
Ben Gotow
Ben Gotow

πŸ›
Rubon72
Rubon72

πŸ›
wanderingme
wanderingme

πŸ›
Thomas Strobl
Thomas Strobl

πŸ› πŸ€” πŸ’‘ πŸ’¬ πŸ‘€
Songkeys
Songkeys

πŸ› πŸ€” πŸ’‘ πŸ’¬ πŸ‘€
AntoineS92
AntoineS92

πŸ›
Sindre Aubert
Sindre Aubert

πŸ›
mx
mx

πŸ›
Sander Heling
Sander Heling

πŸ›
Yida Zhang
Yida Zhang

πŸ› πŸ’»
Nir
Nir

πŸ›
hhatakeyama
hhatakeyama

πŸ›
Paco
Paco

πŸ› πŸ€”
LichLord91
LichLord91

πŸ›
just-small-potato
just-small-potato

πŸ€”
walmsles
walmsles

πŸ›
tenshin
tenshin

πŸ’¬
Steven Tey
Steven Tey

πŸ›
Sergey
Sergey

πŸ› πŸ’» πŸ“–
Diego Azevedo
Diego Azevedo

πŸ“–
Faizan Ahmad
Faizan Ahmad

πŸ›
Kunal L.
Kunal L.

πŸ›
Kevin Wang
Kevin Wang

πŸ€”
u3u
u3u

πŸ€” πŸ‘€
Hong
Hong

πŸ’»

This project follows the all-contributors specification. Contributions of any kind welcome!