iTranslated by AI
Implementing a Minimap in React
I have researched various ways to implement a Minimap in React. By the way, a Minimap is that feature seen in Miro or VSCode that allows you to see your relative position.


Through my research, I found that there are three main methods to implement a Minimap:
- Canvas to Canvas
- HTML to Canvas
- HTML to HTML
Canvas to Canvas
The simplest method is Canvas to Canvas. It feels like an image scaling algorithm where you shrink pixels by a certain scaling factor.
HTML to Canvas
Unfortunately, there are times when you want to create a Minimap for something built with HTML rather than Canvas. In those cases, you would use method 2 or 3.
For method 2, HTML to Canvas, libraries are available.
However, the unpacked size on npm is 2.47MB at the time of writing, so it’s not something you can easily include in the frontend ^^; There might be ways to reduce the size, but I haven't been able to research that fully.
By the way, I believe VSCode also uses this method to create its Minimap, but the source code was too complex for me to decipher. I look forward to hearing from anyone who has managed to decode it.
HTML to HTML
Then there is the third option: HTML to HTML. A library for this is provided for jQuery.
If you look at the source code, you'll see it's incredibly short.
Referenced from the page:
(function( $ ) {
$.fn.minimap = function( $mapSource ) {
var x, y, l, t, w, h;
var $window = $( window );
var $minimap = this;
var minimapWidth = $minimap.width();
var minimapHeight = $minimap.height();
var $viewport = $( "<div></div>" ).addClass( "minimap-viewport" );
$minimap.append( $viewport );
synchronize();
$window.on( "resize", synchronize );
$mapSource.on( "scroll", synchronize );
$mapSource.on( "drag", init );
$minimap.on( "mousedown touchstart", down );
function down( e ) {
var moveEvent, upEvent;
var pos = $minimap.position();
x = Math.round( pos.left + l + w / 2 );
y = Math.round( pos.top + t + h / 2 );
move( e );
if ( e.type === "touchstart" ) {
moveEvent = "touchmove.minimapDown";
upEvent = "touchend";
} else {
moveEvent = "mousemove.minimapDown";
upEvent = "mouseup";
}
$window.on( moveEvent, move );
$window.one( upEvent, up );
}
function move( e ) {
e.preventDefault();
if ( e.type.match( /touch/ ) ) {
if ( e.touches.length > 1 ) {
return;
}
var event = e.touches[ 0 ];
} else {
var event = e;
}
var dx = event.clientX - x;
var dy = event.clientY - y;
if ( l + dx < 0 ) {
dx = -l;
}
if ( t + dy < 0 ) {
dy = -t;
}
if ( l + w + dx > minimapWidth ) {
dx = minimapWidth - l - w;
}
if ( t + h + dy > minimapHeight ) {
dy = minimapHeight - t - h;
}
x += dx;
y += dy;
l += dx;
t += dy;
var coefX = minimapWidth / $mapSource[ 0 ].scrollWidth;
var coefY = minimapHeight / $mapSource[ 0 ].scrollHeight;
var left = l / coefX;
var top = t / coefY;
$mapSource[ 0 ].scrollLeft = Math.round( left );
$mapSource[ 0 ].scrollTop = Math.round( top );
redraw();
}
function up() {
$window.off( ".minimapDown" );
}
function synchronize() {
var dims = [ $mapSource.width(), $mapSource.height() ];
var scroll = [ $mapSource.scrollLeft(), $mapSource.scrollTop() ];
var scaleX = minimapWidth / $mapSource[ 0 ].scrollWidth;
var scaleY = minimapHeight / $mapSource[ 0 ].scrollHeight;
var lW = dims[ 0 ] * scaleX;
var lH = dims[ 1 ] * scaleY;
var lX = scroll[ 0 ] * scaleX;
var lY = scroll[ 1 ] * scaleY;
w = Math.round( lW );
h = Math.round( lH );
l = Math.round( lX );
t = Math.round( lY );
//set the mini viewport dimensions
redraw();
}
function redraw() {
$viewport.css( {
width : w,
height : h,
left : l,
top : t
} );
}
function init() {
$minimap.find( ".minimap-node" ).remove();
//creating mini version of the supplied children
$mapSource.children().each( function() {
var $child = $( this );
var mini = $( "<div></div>" ).addClass( "minimap-node" );
$minimap.append( mini );
var ratioX = minimapWidth / $mapSource[ 0 ].scrollWidth;
var ratioY = minimapHeight / $mapSource[ 0 ].scrollHeight;
var wM = $child.width() * ratioX;
var hM = $child.height() * ratioY;
var xM = ($child.position().left + $mapSource.scrollLeft()) * ratioX;
var yM = ($child.position().top + $mapSource.scrollTop()) * ratioY;
mini.css( {
width : Math.round( wM ),
height : Math.round( hM ),
left : Math.round( xM ),
top : Math.round( yM )
} );
} );
}
init();
return this;
}
})( jQuery );
I also created the same thing using pure JavaScript:
There are also volunteers who have ported this to React. The internal logic is essentially the jQuery minimap library rewritten for React.
However, there are drawbacks to this approach. First, let me explain how the Minimap is implemented.
- Obtain the original Node (DOM) for which you want to create a Minimap. Let's call this "parent."
- Obtain the Node (DOM) where the Minimap will be displayed. Let's call this "minimap."
- Calculate the horizontal and vertical scaling ratios based on the width and height of both the parent and the minimap.
- Use the children API of the Node (DOM) to get the direct child Nodes (DOM) of the parent. Let's call these "children."
- Get the width and height of each child, multiply them by the respective scaling ratios, and calculate the width and height of that element within the minimap.
- Create a new Node (DOM) using
document.createElementand assign it the width and height calculated in the previous step. It is best to assign the same classes as those held by the parent's children. - Finally, append the Node (DOM) created in step 6 into the minimap.
What do you think? It feels like it's brute-forcing the copy-paste using DOM APIs lol.
The drawback is that it uses the children API when implementing the Minimap in steps 5–7. This means only the DOM elements one level deep are reflected. Text written inside them is also not reflected. It cannot accurately copy complex structures, resulting in something featureless. Since the role of a Minimap is to know your relative position within the whole, even a featureless one should fulfill that purpose. If you can accept this, please try implementing a Minimap this way.
I actually wrote a version from scratch in React. Please use it for reference.
Discussion