OpenStreetMap logo OpenStreetMap

Customizing MapLibre Navigation Control for Different Devices and Screen Sizes

Posted by aselnigu on 28 September 2025 in English. Last updated on 29 September 2025.

You can find a German version of this article here: Navigation Control oder Zoom Control in MapLibre

In this post, I’m considering how to customize the Navigation Control and Zoom Control in MapLibre for different scenarios—such as depending on whether a pointer is available or the size of the screen. I’m also wondering whether CSS or JavaScript would be the better approach.

My goal is to display the Navigation Control only on devices where it makes sense and provides a good user experience. Since I’m new to this topic, I welcome any thoughts, feedback, or suggestions for improvement.

I’ll start with a simple example:

<!DOCTYPE html>
<html lang="de">
  <head>
    <title>Demo Navigation Control</title>
    <meta charset="utf-8">
    <meta name="viewport" content="https://wiki.openstreetmap.org/wiki/Tag:width=device-width, https://wiki.openstreetmap.org/wiki/Tag:initial-scale=1">

    <meta name="description" content="Demo Navication Control 1">
    <link
      href="https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.css"
      rel="stylesheet"
    >
    <script
      src="https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.js"
    ></script>
    <link rel="stylesheet" href="index.css">
    <script type="module" src="index.js" defer></script>
  </head>
  <body>
    <div id="map"></div>
  </body>
</html>
const map = new maplibregl.Map({
  container: "map",
  center: [12, 50],
  zoom: 6,
  style: "https://tiles.versatiles.org/assets/styles/colorful/style.json",
});

map.addControl(new maplibregl.NavigationControl({}));
body {
  margin: 0;
  padding: 0;
}

html,
body,
#map {
  height: 100%;
}

Customizing MapLibre Controls for Different Devices and Screen Sizes

This code embeds a simple MapLibre map with a Navigation Control. However, the Navigation Control really only makes sense on devices that support a pointer, allowing users to hover over and click the buttons. On narrow screens, it can even get in the way, so I might prefer not to use it there.

CSS or JavaScript

First, I ask myself: should I solve this problem with JavaScript or CSS?

In my view, if you only want to show the NavigationControl in MapLibre GL JS on devices with a mouse or hover capability, this is more about functionality than design.

  • Design, in my view, describes how something size, colors, layout, position.
  • Function describes how something behaves—when a control is available, who can use it, and what interactions are possible.

The JavaScript line

map.addControl(new maplibregl.NavigationControl({}));

does more than just insert some HTML elements into the DOM that you could hide with CSS:

https://www.openstreetmap.org/user/media (max-width: 768px) {
  .maplibregl-ctrl-compass,
  .maplibregl-ctrl-zoom-out,
  .maplibregl-ctrl-zoom-in {
    display: none !important;
  }
}

The elements still exist in the DOM and bind events, even if they’re invisible.

Of course, there are two sides to this. A designer would surely argue that the placement and visibility of the control are part of the overall look. It can also make sense to use CSS because it reacts automatically to changes—for example, when the screen width changes or a USB mouse is connected later. With JavaScript, you have to handle these cases manually if you want to respond to them.

That’s why it’s worth taking a closer look at both approaches.

CSS

When it comes to showing controls based on input type (touch vs. mouse), any-hover should be used. When it’s about layout and available space, screen width should be considered. It’s also possible to combine both. This way, the controls are only displayed on large screens with hover capability. But let’s go step by step.

Screen Width

At first, I considered hiding the entire control in MapLibre at the top-right corner. However, that would prevent me from adding other controls there without affecting them as well. That’s why I choose to hide all three buttons individually.

https://www.openstreetmap.org/user/media (max-width: 768px) {
  .maplibregl-ctrl-compass,
  .maplibregl-ctrl-zoom-out,
  .maplibregl-ctrl-zoom-in {
    display: none !important;
  }
}

In my opinion, this is the best CSS-only approach. I do have some reservations about using !important, but in my case, it’s the only way it works. I’m not entirely sure why that is, since I can’t find any inline styles. I suspect that my CSS gets overridden when calling map.addControl(new maplibregl.NavigationControl({})).

body {
  margin: 0;
  padding: 0;
}

html,
body,
#map {
  height: 100%;
}

https://www.openstreetmap.org/user/media (max-width: 768px) {
  .maplibregl-ctrl-compass,
  .maplibregl-ctrl-zoom-out,
  .maplibregl-ctrl-zoom-in {
    display: none !important;
  }
}
Device

If my goal isn’t just to hide the controls on narrow screens for space reasons, but to show them only when a pointer or mouse is present and not touch, then I use:

https://www.openstreetmap.org/user/media (any-hover: none), (pointer: coarse) {
  .maplibregl-ctrl-compass,
  .maplibregl-ctrl-zoom-out,
  .maplibregl-ctrl-zoom-in {
    display: none !important;
  }
}

I find that Smashing Magazine explains very clearly why these media queries exist.

The CSS media query any-hover is used to check whether any available input device can hover over elements (i.e., move a mouse pointer or finger over an element).

The pointer media query checks whether the user has a pointing device like a mouse or touchpad and indicates how precise the primary pointing device is.

The combination

https://www.openstreetmap.org/user/media (any-hover: none), (pointer: coarse)

checks whether at least one of the two conditions is true:

  • No input device supports hover: Typical for smartphones or pure touch devices.
  • The primary pointing device is coarse: For example, touchscreens where pixel-precise control isn’t possible and navigation is mainly done with a finger.
Device and Screen Size

The final query combines the two previous approaches:

https://www.openstreetmap.org/user/media (any-hover: none), (pointer: coarse), (max-width: 768px) {
  .maplibregl-ctrl-compass,
  .maplibregl-ctrl-zoom-out,
  .maplibregl-ctrl-zoom-in {
    display: none !important;
  }
}

JavaScript

If you want to display the MapLibre NavigationControl only under certain conditions—such as depending on screen width or whether a mouse is connected—you can control it directly with JavaScript.

if (
  window.matchMedia("(any-hover: hover)").matches &&
  !window.matchMedia("(pointer: coarse)").matches &&
  window.matchMedia("(min-width: 769px)").matches
) {
  map.addControl(new maplibregl.NavigationControl());
}

This way, you avoid CSS hacks like display: none, where the control is still initialized and binds events. Instead, the control is only added to the map when it’s actually needed.

The approach above, however, doesn’t respond dynamically to changes. This can also be handled with JavaScript:

const navigationControl = new maplibregl.NavigationControl();

function updateNavigationControl() {
  const shouldShowNavigation =
    window.matchMedia("(any-hover: hover)").matches &&
    !window.matchMedia("(pointer: coarse)").matches &&
    window.matchMedia("(min-width: 769px)").matches;

  const isAdded = navigationControl._container?.parentNode https://wiki.openstreetmap.org/wiki/Tag:!== null;

  if (shouldShowNavigation && !isAdded) {
    map.addControl(navigationControl, "top-right");
  } else if (!shouldShowNavigation && isAdded) {
    map.removeControl(navigationControl);
  }
}

updateNavigationControl();

window.matchMedia("(min-width: 769px)").addEventListener("change", updateNavigationControl);
window.matchMedia("(any-hover: hover)").addEventListener("change", updateNavigationControl);
window.matchMedia("(pointer: coarse)").addEventListener("change", updateNavigationControl);

Immediately after loading, the code checks whether the control should be displayed. Using window.matchMedia, it queries device properties, such as:

  • Screen width
  • Input method (mouse or touch)

With addEventListener("change", …), the code reacts dynamically.

The line

const isAdded = navigationControl._container?.parentNode https://wiki.openstreetmap.org/wiki/Tag:!== null;

checks directly in the DOM whether the control is currently active on the map. In my experience, this works reliably. However, _container is an internal property of MapLibre and is not officially documented. If one wants to avoid using internal properties entirely, one can use an own flag instead.

Example code is available on CodePen or Codeberg.

Email icon Bluesky Icon Facebook Icon LinkedIn Icon Mastodon Icon Telegram Icon X Icon

Discussion

Log in to leave a comment