JSFiddle - React, Tailwind, and code Playground
HTML
<div class="image-zoom">
<img src="//i.imgur.com/qnwhFrQ.png" width="771" height="480" alt="">
<div class="image-zoom-block"></div>
</div>
<div class="image-zoom">
<img src="//i.imgur.com/Ffa64Ma.gif" width="200" height="385" alt="">
<div class="image-zoom-block"></div>
</div>
CSS
body {display: flex; align-items: flex-start;}
.image-zoom:nth-child(1) {width: 75%}
.image-zoom {
display: inline-block;
max-width: 100%;
max-height: 100%;
height: max-content;
position: relative;
cursor: zoom-in;
}
.image-zoom img {
display: block;
width: 100%;
height: auto;
border: 0;
margin: 0;
position: relative;
z-index: 1;
}
.image-zoom-block {
display: none;
width: 50%;
height: 50%;
background-image: var(--image);
background-repeat: no-repeat;
background-size: 600%;
background-position: var(--x) var(--y);
position: absolute;
left: var(--new-x);
top: var(--new-y);
transform: translateX(-50%) translateY(-50%);
z-index: 2;
pointer-events: none;
}
.image-zoom.-active {
cursor: zoom-out;
}
.image-zoom.-enter .image-zoom-block {
display: block;
}
JavaScript
let zoom = document.querySelectorAll('.image-zoom');
zoom.forEach(function(el) {
el.addEventListener('click', function(e) {
const target = e.target.closest('.image-zoom'),
rect = target.getBoundingClientRect();
target.classList.toggle('-active');
target.style.setProperty('--image', `url(${target.querySelector('img').getAttribute('src')})`);
target.style.setProperty('--x', Math.floor(((e.clientX - rect.left) / rect.width * 100) * 100) / 100+'%');
target.style.setProperty('--y', Math.floor(((e.clientY - rect.top) / rect.height * 100) * 100) / 100+'%');
target.style.setProperty('--new-x', e.clientX - rect.left + 'px');
target.style.setProperty('--new-y', e.clientY - rect.top + 'px');
target.classList.toggle('-enter');
});
el.addEventListener('mouseenter', function(e) {
const target = e.target.closest('.image-zoom');
if(target.classList.contains('-active')) {
target.classList.add('-enter');
}
});
el.addEventListener('mousemove', function(e) {
const target = e.target.closest('.image-zoom');
if(target.classList.contains('-active')) {
const rect = target.getBoundingClientRect();
target.style.setProperty('--x', Math.floor(((e.clientX - rect.left) / rect.width * 100) * 100) / 100+'%');
target.style.setProperty('--y', Math.floor(((e.clientY - rect.top) / rect.height * 100) * 100) / 100+'%');
target.style.setProperty('--new-x', e.clientX - rect.left + 'px');
target.style.setProperty('--new-y', e.clientY - rect.top + 'px');
}
});
el.addEventListener('mouseleave', function(e) {
let target = e.target.closest('.image-zoom');
if(target.classList.contains('-active')) {
target.classList.remove('-enter');
}
});
});