Place interactive markers on image with React

616 Views Asked by At

Help please. I have an image like this image below to display on a home page ( a much larger one). Notice that there is a white and blue marker with a black arrow in the middle on the "FORGE" house.

The same for the "Rentals" house.

Each one of these markers will behave as an interactive button. Here is the expected behavior on interaction:

  • When hover, the blue background will expand to show a text like "Forge" or "Rentals"
  • When clicked, it will navigate user to the respective page "Forge" or "Rentals".

Also would like it to be responsive, if when the screen resizes, the buttons should remain on these specific position.

I was hacking around with absolute positioning, x, y coordinates to try to make it work. But can't come up to a working solution due to my like of experience with these kind of UI. Not sure if I should use a canvas or other approach.

Any help would be much appreciated.

My code look like this, but looks like I'm not on the right track:

const ImageComponent = () => {

  const markers = [
    { name: 'Forge', x: 100, y: 200 },
    { name: 'Rentals', x: 300, y: 150 },
    // Add more 
  ];

  const handleMarkerClick = (m) => {
    // do something with marker
  };

  return (
    <div style={{ position: 'relative' }}>
      <img src="path/to/image.jpg" alt="Image with markers" />

      {markers.map((marker, index) => (
        <div
          key={index}
          className="marker"
          style={{ left: marker.x, top: marker.y, position: "absolute" }}
          onClick={() => handleMarkerClick(marker)}
        />
      ))}
    </div>
  );
};

enter image description here

1

There are 1 best solutions below

0
On

You should place controls with position absolute to container of image. Implementation depends on multiple things like where you put your image, is it goes fullscreen or not, do you have something before image or after, etc. But this code should show you main principles.

If image is being resized while you resize your window you should make a container which will behave exactly like an image. Then you can set position of controls relative to this container and get them stick to their points.

.container {
  width: 60vw; /* set 100vw to fit image window width*/
  position: relative;
}
.container img { width: 100%; display: block;}
.container .control {
  position: absolute;
  width: 7.5%;  /* size in percents to resize controls with image */
  height: 7%;
  border-radius: 200px;
  background: red;
  transition: all 0.2s ease-in-out;
}

.container .control:hover {
  width: 20%;
}

#c1 { left: 50.4%; top: 26%; } /* position in percents, not in pixels */
#c2 { left: 58.5%; top: 76.3%; }
<div class="container">
  <img src="https://i.stack.imgur.com/BVaY9.jpg"/>
  <div class="control" id="c1"></div>
  <div class="control" id="c2"></div>
</div>

With your code it will be something like this (but I recommend to move your styles in style file, learn how to use module styles in react):

const ImageComponent = () => {

  const markers = [
    { name: 'Forge', x: 50.4, y: 26 },
    { name: 'Rentals', x: 58.5, y: 76.3 },
    // Add more 
  ];

  const handleMarkerClick = (m) => {
    // do something with marker
  };

  return (
    <div style={{ position: 'relative', width: '100vw' }}>
      <img style={{ width: '100%', display: 'block'}} src="path/to/image.jpg" alt="Image with markers" />

      {markers.map((marker, index) => (
        <div
          key={index}
          className="marker"
          style={{ left: `${marker.x}%`, top: `${marker.y}%`, position: "absolute" }}
          onClick={() => handleMarkerClick(marker)}
        />
      ))}
    </div>
  );
};

Just fix positions and add normal control view.

For control you should make separate component. Here is an example how to achieve the behaviour you want:

.control {
  background: #08f;
  padding: 10px;
  width: 60px;
  box-sizing: border-box;
  border-radius: 200px;
  transition: all 0.2s ease-in-out;
  display: flex;
  align-items: center;
  overflow: hidden;
  cursor: pointer;
}

.control:hover {
  width: 200px;
}

.text {
  color: white;
  font: 30px Arial;
  margin-left: 16px;
  opacity: 0;
  transition: opacity 0.2s ease-in-out;
}

.control:hover .text {
  opacity: 1;
}

.icon {
  width: 40px;
  min-width: 40px;
  height: 40px;
  border-radius: 50%;
  box-shadow: 2px 2px 7px #0005;
  background-color: white;
  background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIFVwbG9hZGVkIHRvOiBTVkcgUmVwbywgd3d3LnN2Z3JlcG8uY29tLCBHZW5lcmF0b3I6IFNWRyBSZXBvIE1peGVyIFRvb2xzIC0tPgo8IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPgo8c3ZnIGZpbGw9IiMwMDAwMDAiIGhlaWdodD0iODAwcHgiIHdpZHRoPSI4MDBweCIgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiAKCSB2aWV3Qm94PSIwIDAgMjk3IDI5NyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxnPgoJPHBhdGggZD0iTTI5NC4wNzcsMjUxLjE5OWwtNTkuMTA1LTU5LjEwN2w0Mi4xNjctMjQuMzU2YzMuMjk1LTEuOTAzLDUuMjEyLTUuNTIsNC45MzgtOS4zMTVjLTAuMjc0LTMuNzk2LTIuNjkyLTcuMTAxLTYuMjI2LTguNTEKCQlMODcuODIsNzQuOTA1Yy0zLjY4Ni0xLjQ3Mi03Ljg5NS0wLjYwNS0xMC43MDIsMi4yMDFjLTIuODA3LDIuODA4LTMuNjc0LDcuMDE2LTIuMjAzLDEwLjcwMmw3NC45OTQsMTg4LjA1MwoJCWMxLjQxLDMuNTM0LDQuNzE1LDUuOTUzLDguNTExLDYuMjI3YzMuNzk2LDAuMjc2LDcuNDE0LTEuNjQyLDkuMzE2LTQuOTM4bDI0LjM1NC00Mi4xNjdsNTkuMTAxLDU5LjEwNwoJCWMxLjg2MiwxLjg2Myw0LjM5LDIuOTEsNy4wMjMsMi45MWMyLjYzNSwwLDUuMTYxLTEuMDQ3LDcuMDIzLTIuOTFsMjguODQxLTI4Ljg0NUMyOTcuOTU2LDI2MS4zNjYsMjk3Ljk1NiwyNTUuMDc4LDI5NC4wNzcsMjUxLjE5OQoJCXoiLz4KCTxwYXRoIGQ9Ik00My42MSwyOS41NTJjLTMuODc5LTMuODc2LTEwLjE2Ni0zLjg3Ny0xNC4wNDcsMGMtMy44NzgsMy44NzktMy44NzgsMTAuMTY4LDAsMTQuMDQ3bDIyLjA2OSwyMi4wNjkKCQljMS45MzksMS45MzksNC40OCwyLjkwOSw3LjAyMywyLjkwOWMyLjU0MSwwLDUuMDgzLTAuOTcsNy4wMjItMi45MDljMy44NzktMy44NzksMy44NzktMTAuMTY3LDAtMTQuMDQ2TDQzLjYxLDI5LjU1MnoiLz4KCTxwYXRoIGQ9Ik01MS4wODksOTguMjE1YzAtNS40ODQtNC40NDctOS45MzItOS45MzMtOS45MzJIOS45NDZjLTUuNDg1LDAtOS45MzMsNC40NDctOS45MzMsOS45MzJjMCw1LjQ4NSw0LjQ0Nyw5LjkzMyw5LjkzMyw5LjkzMwoJCWgzMS4yMUM0Ni42NDIsMTA4LjE0Nyw1MS4wODksMTAzLjcsNTEuMDg5LDk4LjIxNXoiLz4KCTxwYXRoIGQ9Ik00Ny4wNjMsMTI4Ljg2OWwtMjIuMDcyLDIyLjA3MWMtMy44NzgsMy44NzktMy44NzgsMTAuMTY4LDAsMTQuMDQ2YzEuOTQsMS45MzksNC40ODIsMi45MDksNy4wMjMsMi45MDkKCQljMi41NDEsMCw1LjA4NC0wLjk3LDcuMDIzLTIuOTA5bDIyLjA3MS0yMi4wN2MzLjg3OS0zLjg3OSwzLjg3OS0xMC4xNjgsMC0xNC4wNDdDNTcuMjMsMTI0Ljk5Myw1MC45NDQsMTI0Ljk5Miw0Ny4wNjMsMTI4Ljg2OXoiLz4KCTxwYXRoIGQ9Ik05OC4yMjIsNTEuMDc4YzUuNDg1LDAsOS45MzMtNC40NDcsOS45MzMtOS45MzNWOS45MzJjMC01LjQ4NS00LjQ0Ny05LjkzMi05LjkzMy05LjkzMmMtNS40ODQsMC05LjkzMiw0LjQ0Ni05LjkzMiw5LjkzMgoJCXYzMS4yMTRDODguMjksNDYuNjMxLDkyLjczNyw1MS4wNzgsOTguMjIyLDUxLjA3OHoiLz4KCTxwYXRoIGQ9Ik0xMzUuODk0LDY0LjAwNmMyLjU0MywwLDUuMDg0LTAuOTcsNy4wMjMtMi45MDlsMjIuMDY4LTIyLjA2OWMzLjg3OS0zLjg3OSwzLjg3OS0xMC4xNjgsMC0xNC4wNDcKCQljLTMuODc5LTMuODc3LTEwLjE2OC0zLjg3Ny0xNC4wNDYsMGwtMjIuMDY4LDIyLjA3Yy0zLjg3OSwzLjg3OS0zLjg3OSwxMC4xNjgsMCwxNC4wNDYKCQlDMTMwLjgxMSw2My4wMzYsMTMzLjM1Miw2NC4wMDYsMTM1Ljg5NCw2NC4wMDZ6Ii8+CjwvZz4KPC9zdmc+');
  background-size: 60%;
  background-repeat: no-repeat;
  background-position: center;
}
<div class="control">
  <div class="icon"></div>
  <div class="text">Home</div>
</div>