[html] Responsive image map

I have an existing image map in a responsive html layout. Images scale according to browser size, but the image coordinates are obviously fixed pixel sizes. What options do I have to resize the image map coordinates?

This question is related to html css imagemap

The answer is


David Bradshaw wrote a nice little library that solves this problem. It can be used with or without jQuery.

Available here: https://github.com/davidjbradshaw/imagemap-resizer


http://home.comcast.net/~urbanjost/semaphore.html is the top page for the discussion, and actually has links to a JavaScript-based solution to the problem. I have received a notice that HTML will support percent units in the future but I haven't seen any progress on this in quite some time (it has probably been over a year since I heard support would be forthcoming) so the work-around is probably worth looking at if you are comfortable with JavaScript/ECMAScript.



It depends, you can use jQuery to adjust the ranges proportionally I think. Why do you use an image map by the way? Can't you use scaling divs or other elements for it?


I have created a javascript version of the solution Tom Bisciglia suggested.

My code allows you to use a normal image map. All you have to do is load a few lines of CSS and a few lines of JS and... BOOM... your image map has hover states and is fully responsive! Magic right?

_x000D_
_x000D_
var images = document.querySelectorAll('img[usemap]');
images.forEach( function(image) {
    var mapid = image.getAttribute('usemap').substr(1);
    var imagewidth = image.getAttribute('width');
    var imageheight = image.getAttribute('height');
    var imagemap = document.querySelector('map[name="'+mapid+'"]');
    var areas = imagemap.querySelectorAll('area');

    image.removeAttribute('usemap');
    imagemap.remove();

    // create wrapper container
    var wrapper = document.createElement('div');
    wrapper.classList.add('imagemap');
    image.parentNode.insertBefore(wrapper, image);
    wrapper.appendChild(image);

    areas.forEach( function(area) {
        var coords = area.getAttribute('coords').split(',');
        var xcoords = [parseInt(coords[0]),parseInt(coords[2])];
        var ycoords = [parseInt(coords[1]),parseInt(coords[3])];
        xcoords = xcoords.sort(function(a, b){return a-b});
        ycoords = ycoords.sort(function(a, b){return a-b});
        wrapper.innerHTML += "<a href='"+area.getAttribute('href')+"' title='"+area.getAttribute('title')+"' class='area' style='left: "+((xcoords[0]/imagewidth)*100).toFixed(2)+"%; top: "+((ycoords[0]/imageheight)*100).toFixed(2)+"%; width: "+(((xcoords[1] - xcoords[0])/imagewidth)*100).toFixed(2)+"%; height: "+(((ycoords[1] - ycoords[0])/imageheight)*100).toFixed(2)+"%;'></a>";
    });
});
_x000D_
img {max-width: 100%; height: auto;}

.imagemap {position: relative;}
.imagemap img {display: block;}
.imagemap .area {display: block; position: absolute; transition: box-shadow 0.15s ease-in-out;}
.imagemap .area:hover {box-shadow: 0px 0px 1vw rgba(0,0,0,0.5);}
_x000D_
<!-- Image Map Generated by http://www.image-map.net/ -->
<img src="https://i.imgur.com/TwmCyCX.jpg" width="2000" height="2604" usemap="#image-map">
<map name="image-map">
    <area target="" alt="Zirconia Abutment" title="Zirconia Abutment" href="/" coords="3,12,199,371" shape="rect">
    <area target="" alt="Gold Abutment" title="Gold Abutment" href="/" coords="245,12,522,371" shape="rect">
    <area target="" alt="CCM Abutment" title="CCM Abutment" href="/" coords="564,12,854,369" shape="rect">
    <area target="" alt="EZ Post Abutment" title="EZ Post Abutment" href="/" coords="1036,12,1360,369" shape="rect">
    <area target="" alt="Milling Abutment" title="Milling Abutment" href="/" coords="1390,12,1688,369" shape="rect">
    <area target="" alt="Angled Abutment" title="Angled Abutment" href="/" coords="1690,12,1996,371" shape="rect">
    <area target="" alt="Temporary Abutment [Metal]" title="Temporary Abutment [Metal]" href="/" coords="45,461,506,816" shape="rect">
    <area target="" alt="Fuse Abutment" title="Fuse Abutment" href="/" coords="1356,461,1821,816" shape="rect">
    <area target="" alt="Lab Analog" title="Lab Analog" href="/" coords="718,935,1119,1256" shape="rect">
    <area target="" alt="Transfer Impression Coping Driver" title="Transfer Impression Coping Driver" href="/" coords="8,1330,284,1731" shape="rect">
    <area target="" alt="Impression Coping [Transfer]" title="Impression Coping [Transfer]" href="/" coords="310,1330,697,1731" shape="rect">
    <area target="" alt="Impression Coping [Pick-Up]" title="Impression Coping [Pick-Up]" href="/" coords="1116,1330,1560,1733" shape="rect">
</map>
_x000D_
_x000D_
_x000D_


Check out the image-map plugin on Github. It works both with vanilla JavaScript and as a jQuery plugin.

$('img[usemap]').imageMap();     // jQuery

ImageMap('img[usemap]')          // JavaScript

Check out the demo.


For some reason none of these solutions worked for me. I've had the best success using transforms.

transform: translateX(-5.8%) translateY(-5%) scale(0.884);

For those who don't want to resort to JavaScript, here's an image slicing example:

http://codepen.io/anon/pen/cbzrK

As you scale the window, the clown image will scale accordingly, and when it does, the nose of the clown remains hyperlinked.


Similar to Orland's answer here: https://stackoverflow.com/a/32870380/462781

Combined with Chris' code here: https://stackoverflow.com/a/12121309/462781

If the areas fit in a grid you can overlay the areas by transparent pictures using a width in % that keep their aspect ratio.

_x000D_
_x000D_
    .wrapperspace {_x000D_
      width: 100%;_x000D_
      display: inline-block;_x000D_
      position: relative;_x000D_
    }_x000D_
    .mainspace {_x000D_
      position: absolute;_x000D_
      top: 0;_x000D_
      bottom: 0;_x000D_
      right: 0;_x000D_
      left: 0;_x000D_
    }
_x000D_
<div class="wrapperspace">_x000D_
 <img style="float: left;" title="" src="background-image.png" width="100%" />_x000D_
 <div class="mainspace">_x000D_
     <div>_x000D_
         <img src="space-top.png" style="margin-left:6%;width:15%;"/>_x000D_
     </div>_x000D_
     <div>_x000D_
       <a href="http://www.example.com"><img src="space-company.png" style="margin-left:6%;width:15%;"></a>_x000D_
     </div>_x000D_
  <div>_x000D_
   <a href="http://www.example.com"><img src="space-company.png" style="margin-left:6%;width:10%;"></a>_x000D_
   <a href="http://www.example.com"><img src="space-company.png" style="width:20%;"></a>_x000D_
  </div>_x000D_
 </div>_x000D_
</div>
_x000D_
_x000D_
_x000D_

You can use a margin in %. Additionally "space" images can be placed next to each other inside a 3rd level div.


I ran across a solution that doesn't use image maps at all but rather anchor tags that are absolutely positioned over the image. The only drawback would be that the hotspot would have to be rectangular, but the plus is that this solution doesn't rely on Javascript, just CSS. There is a website that you can use to generate the HTML code for the anchors: http://www.zaneray.com/responsive-image-map/

I put the image and the generated anchor tags in a relatively positioned div tag and everything worked perfectly on window resize and on my mobile phone.


responsive width && height

window.onload = function () {
        var ImageMap = function (map, img) {
                var n,
                    areas = map.getElementsByTagName('area'),
                    len = areas.length,
                    coords = [],
                    imgWidth = img.naturalWidth,
                    imgHeight = img.naturalHeight;
                for (n = 0; n < len; n++) {
                    coords[n] = areas[n].coords.split(',');
                }
            this.resize = function () {
                var n, m, clen,
                    x = img.offsetWidth / imgWidth,
                    y = img.offsetHeight / imgHeight;
                    imgWidth = img.offsetWidth;
                    imgHeight = img.offsetHeight;
                for (n = 0; n < len; n++) {
                    clen = coords[n].length;
                    for (m = 0; m < clen; m +=2) {
                        coords[n][m] *= x;
                        coords[n][m+1] *= y;
                    }
                    areas[n].coords = coords[n].join(',');
                }
                    return true;
                };
                window.onresize = this.resize;
            },
        imageMap = new ImageMap(document.getElementById('map_region'), document.getElementById('prepay_region'));
        imageMap.resize();
        return;
    }

You can also use svg instead of an image map. ;)

There is a tutorial on how to do this.

_x000D_
_x000D_
.hover_group:hover {_x000D_
  opacity: 1;_x000D_
}_x000D_
#projectsvg {_x000D_
  position: relative;_x000D_
  width: 100%;_x000D_
  padding-bottom: 77%;_x000D_
  vertical-align: middle;_x000D_
  margin: 0;_x000D_
  overflow: hidden;_x000D_
}_x000D_
#projectsvg svg {_x000D_
  display: inline-block;_x000D_
  position: absolute;_x000D_
  top: 0;_x000D_
  left: 0;_x000D_
}
_x000D_
<figure id="projectsvg">_x000D_
  <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1920 1080" preserveAspectRatio="xMinYMin meet" >_x000D_
<!-- set your background image -->_x000D_
<image width="1920" height="1080" xlink:href="http://placehold.it/1920x1080" />_x000D_
<g class="hover_group" opacity="0">_x000D_
  <a xlink:href="https://example.com/link1.html">_x000D_
    <text x="652" y="706.9" font-size="20">First zone</text>_x000D_
    <rect x="572" y="324.1" opacity="0.2" fill="#FFFFFF" width="264.6" height="387.8"></rect>_x000D_
  </a>_x000D_
</g>_x000D_
<g class="hover_group" opacity="0">_x000D_
  <a xlink:href="https://example.com/link2.html">_x000D_
    <text x="1230.7" y="952" font-size="20">Second zone</text>_x000D_
    <rect x="1081.7" y="507" opacity="0.2" fill="#FFFFFF" width="390.2" height="450"></rect>_x000D_
  </a>_x000D_
</g>_x000D_
  </svg>_x000D_
</figure>
_x000D_
_x000D_
_x000D_


The following method works perfectly for me, so here's my full implementation:

<img id="my_image" style="display: none;" src="my.png" width="924" height="330" border="0" usemap="#map" />

<map name="map" id="map">
    <area shape="poly" coords="774,49,810,21,922,130,920,222,894,212,885,156,874,146" href="#mylink" />
    <area shape="poly" coords="649,20,791,157,805,160,809,217,851,214,847,135,709,1,666,3" href="#myotherlink" />
</map>

<script>
$(function(){
    var image_is_loaded = false;
    $("#my_image").on('load',function() {
        $(this).data('width', $(this).attr('width')).data('height', $(this).attr('height'));
        $($(this).attr('usemap')+" area").each(function(){
            $(this).data('coords', $(this).attr('coords'));
        });

        $(this).css('width', '100%').css('height','auto').show();

        image_is_loaded = true;
        $(window).trigger('resize');
    });


    function ratioCoords (coords, ratio) {
        coord_arr = coords.split(",");

        for(i=0; i < coord_arr.length; i++) {
            coord_arr[i] = Math.round(ratio * coord_arr[i]);
        }

        return coord_arr.join(',');
    }
    $(window).on('resize', function(){
        if (image_is_loaded) {
            var img = $("#my_image");
            var ratio = img.width()/img.data('width');

            $(img.attr('usemap')+" area").each(function(){
                console.log('1: '+$(this).attr('coords'));
                $(this).attr('coords', ratioCoords($(this).data('coords'), ratio));
            });
        }
    });
});
</script>

Working for me (remember to change 3 things in code):

  • previousWidth (original size of image)

  • map_ID (id of your image map)

  • img_ID (id of your image)

HTML:

<div style="width:100%;">
    <img id="img_ID" src="http://www.gravatar.com/avatar/0865e7bad648eab23c7d4a843144de48?s=128&d=identicon&r=PG" usemap="#map" border="0" width="100%" alt="" />
</div>
<map id="map_ID" name="map">
<area shape="poly" coords="48,10,80,10,65,42" href="javascript:;" alt="Bandcamp" title="Bandcamp" />
<area shape="poly" coords="30,50,62,50,46,82" href="javascript:;" alt="Facebook" title="Facebook" />
<area shape="poly" coords="66,50,98,50,82,82" href="javascript:;" alt="Soundcloud" title="Soundcloud" />
</map>

Javascript:

window.onload = function () {
    var ImageMap = function (map, img) {
            var n,
                areas = map.getElementsByTagName('area'),
                len = areas.length,
                coords = [],
                previousWidth = 128;
            for (n = 0; n < len; n++) {
                coords[n] = areas[n].coords.split(',');
            }
            this.resize = function () {
                var n, m, clen,
                    x = img.offsetWidth / previousWidth;
                for (n = 0; n < len; n++) {
                    clen = coords[n].length;
                    for (m = 0; m < clen; m++) {
                        coords[n][m] *= x;
                    }
                    areas[n].coords = coords[n].join(',');
                }
                previousWidth = img.offsetWidth;
                return true;
            };
            window.onresize = this.resize;
        },
        imageMap = new ImageMap(document.getElementById('map_ID'), document.getElementById('img_ID'));
    imageMap.resize();
    return;
}

JSFiddle: http://jsfiddle.net/p7EyT/154/


I come across with same requirement where, I wants to show responsive image map which can resize with any screen size and important thing is, i want to highlight that coordinates.

So i tried many libraries which can resize coordinates according to screen size and event. And i got best solution(jquery.imagemapster.min.js) which works fine with almost all browsers. Also i have integrated it with Summer Plgin which create image map.

_x000D_
_x000D_
 var resizeTime = 100;
 var resizeDelay = 100;    

$('img').mapster({
        areas: [
            {
                key: 'tbl',
                fillColor: 'ff0000',
                staticState: true,
                stroke: true
            }
        ],
        mapKey: 'state'
    });

    // Resize the map to fit within the boundaries provided

    function resize(maxWidth, maxHeight) {
        var image = $('img'),
            imgWidth = image.width(),
            imgHeight = image.height(),
            newWidth = 0,
            newHeight = 0;

        if (imgWidth / maxWidth > imgHeight / maxHeight) {
            newWidth = maxWidth;
        } else {
            newHeight = maxHeight;
        }
        image.mapster('resize', newWidth, newHeight, resizeTime);
    }

    function onWindowResize() {

        var curWidth = $(window).width(),
            curHeight = $(window).height(),
            checking = false;
        if (checking) {
            return;
        }
        checking = true;
        window.setTimeout(function () {
            var newWidth = $(window).width(),
                newHeight = $(window).height();
            if (newWidth === curWidth &&
                newHeight === curHeight) {
                resize(newWidth, newHeight);
            }
            checking = false;
        }, resizeDelay);
    }

    $(window).bind('resize', onWindowResize);
_x000D_
img[usemap] {
        border: none;
        height: auto;
        max-width: 100%;
        width: auto;
    }
_x000D_
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.imagemapster.min.js"></script>

<img src="https://discover.luxury/wp-content/uploads/2016/11/Cities-With-the-Most-Michelin-Star-Restaurants-1024x581.jpg" alt="" usemap="#map" />
<map name="map">
    <area shape="poly" coords="777, 219, 707, 309, 750, 395, 847, 431, 916, 378, 923, 295, 870, 220" href="#" alt="poly" title="Polygon" data-maphilight='' state="tbl"/>
    <area shape="circle" coords="548, 317, 72" href="#" alt="circle" title="Circle" data-maphilight='' state="tbl"/>
    <area shape="rect" coords="182, 283, 398, 385" href="#" alt="rect" title="Rectangle" data-maphilight='' state="tbl"/>
</map>
_x000D_
_x000D_
_x000D_

Hope help it to someone.


I found a no-JS way to address this if you are okay with rectangular hit areas.

First of all, make sure your image is in a div that's relatively positioned. Then put the image inside this div, which means it'll take up all the space in the div. Finally, add absolutely positioned div's under the image, within the main div, and use percentages for top, left, width, and height to get the link hit areas the size and position you want.

I find it's easiest to give the div a black background color (ideally with some alpha fading so you can see the linked content underneath) when you're first working, and to use a code inspector in your browser to adjust the percentages in real time, so that you can get it just right.

Here's the basic outline you can work with. By doing everything with percentages, you ensure the elements all stay the same relative size and position as the image scales.

<div style="position: relative;">
  <img src="background-image.png" style="width: 100%; height: auto;">
  <a href="/link1"><div style="position: absolute; left: 15%; top: 20%; width: 12%; height: 8%; background-color: rgba(0, 0, 0, .25);"></div></a>
  <a href="/link2"><div style="position: absolute; left: 52%; top: 38%; width: 14%; height: 20%; background-color: rgba(0, 0, 0, .25);"></div></a>
</div>

Use this code with your code inspector in Chrome or your browser of choice, and adjust the percentages (you can use decimal percentages to be more exact) until the boxes are just right. Also choose a background-color of transparent when you're ready to use it since you want your hit areas to be invisible.