JSFiddle - React, Tailwind, and code Playground

HTML

<script src="https://netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
<body>
  <div class="container">
    <h1>Image Zoom</h1>
    <p>
      Trust fund seitan chia, wolf lomo letterpress Bushwick before they sold out. Carles kogi fixie, squid twee Tonx readymade cred typewriter scenester locavore kale chips vegan organic. Meggings pug wolf Shoreditch typewriter skateboard. McSweeney's iPhone
      chillwave, food truck direct trade disrupt flannel irony tousled Cosby sweater single-origin coffee. Organic disrupt bicycle rights, tattooed messenger bag flannel craft beer fashion axe bitters. Readymade sartorial craft beer, quinoa sustainable
      butcher Marfa Echo Park Terry Richardson gluten-free flannel retro cred mlkshk banjo. Salvia 90's art party Blue Bottle, PBR&B cardigan 8-bit.
    </p>
    <p>
      Meggings irony fashion axe, tattooed master cleanse Blue Bottle stumptown bitters authentic flannel freegan paleo letterpress ugh sriracha. Wolf PBR&B art party aesthetic meh cliche. Sartorial before they sold out deep v, aesthetic PBR&B craft beer post-ironic
      synth keytar pork belly skateboard pour-over. Tonx cray pug Etsy, gastropub ennui wolf ethnic Odd Future viral master cleanse skateboard banjo. Pitchfork scenester cornhole, whatever try-hard ethnic banjo +1 gastropub American Apparel vinyl skateboard
      Shoreditch seitan. Blue Bottle keffiyeh Austin Helvetica art party. Portland ethnic fixie, beard retro direct trade ugh scenester Tumblr readymade authentic plaid pickled hashtag biodiesel.
    </p>
    <img src="https://fat.github.io/zoom.js/img/palm.jpg" data-original="https://www.rodiaki.gr/medias/2017/02/28/dd219ad13d56b149e2d5237b87a78350.jpg?tt=81230be5ceef02a94c0308fe59987a4e" data-action="zoom">
    <p>
      Thundercats freegan Truffaut, four loko twee Austin scenester lo-fi seitan High Life paleo quinoa cray. Schlitz butcher...

CSS

/**  zoom.css  **/

img[data-action="zoom"] {
  cursor: pointer;
  cursor: -webkit-zoom-in;
  cursor: -moz-zoom-in;
}

.zoom-img,
.zoom-img-wrap {
  position: relative;
  z-index: 666;
  -webkit-transition: all 300ms;
  -o-transition: all 300ms;
  transition: all 300ms;
}

img.zoom-img {
  cursor: pointer;
  cursor: -webkit-zoom-out;
  cursor: -moz-zoom-out;
}

.zoom-overlay {
  z-index: 420;
  background: #fff;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  pointer-events: none;
  filter: "alpha(opacity=0)";
  opacity: 0;
  -webkit-transition: opacity 300ms;
  -o-transition: opacity 300ms;
  transition: opacity 300ms;
}

.zoom-overlay-open .zoom-overlay {
  filter: "alpha(opacity=100)";
  opacity: 1;
}

.zoom-overlay-open,
.zoom-overlay-transitioning {
  cursor: default;
}


/****** end zoom.css *****/


/* SIMPLE DEMO STYLES */

body {
  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-size: 12px;
  line-height: 1.6;
}

.container {
  margin: 50px;
  max-width: 700px;
}

.container img {
  width: 100%;
}

.container .pull-left {
  width: 55%;
  float: left;
  margin: 20px 20px 20px -80px;
}

@media (min-width: 750px) {
  body {
    font-size: 16px;
    line-height: 1.6;
  }
  .container {
    margin: 100px auto;
  }
}

JavaScript

;
(function($) {
  "use strict";

  /**
   * The zoom service
   */
  function ZoomService() {
    this._activeZoom =
      this._initialScrollPosition =
      this._initialTouchPosition =
      this._touchMoveListener = null

    this._$document = $(document)
    this._$window = $(window)
    this._$body = $(document.body)

    this._boundClick = $.proxy(this._clickHandler, this)
  }

  ZoomService.prototype.listen = function() {
    this._$body.on('click', '[data-action="zoom"]', $.proxy(this._zoom, this))
  }

  ZoomService.prototype._zoom = function(e) {
    var target = e.target

    if (!target || target.tagName != 'IMG') return

    if (this._$body.hasClass('zoom-overlay-open')) return

    if (e.metaKey || e.ctrlKey) {
      return window.open((e.target.getAttribute('data-original') || e.target.src), '_blank')
    }

    if (target.width >= ($(window).width() - Zoom.OFFSET)) return

    this._activeZoomClose(true)

    this._activeZoom = new Zoom(target)
    this._activeZoom.zoomImage()

    // todo(fat): probably worth throttling this
    this._$window.on('scroll.zoom', $.proxy(this._scrollHandler, this))

    this._$document.on('keyup.zoom', $.proxy(this._keyHandler, this))
    this._$document.on('touchstart.zoom', $.proxy(this._touchStart, this))

    // we use a capturing phase here to prevent unintended js events
    // sadly no useCapture in jquery api (http://bugs.jquery.com/ticket/14953)
    if (document.addEventListener) {
      document.addEventListener('click', this._boundClick, true)
    } else {
      document.attachEvent('onclick', this._boundClick, true)
    }

    if ('bubbles' in e) {
      if (e.bubbles) e.stopPropagation()
    } else {
      // Internet Explorer before version 9
      e.cancelBubble = true
    }
  }

  ZoomService.prototype._activeZoomClose = function(forceDispose) {
    if (!this._activeZoom) return

    if (forceDispose) {
      this._activeZoom.dispose()
    } else {
      this._activeZoom.close()
    }

   ...