Skip to content

KristjanESPERANTO/leaflet-theme-control

Repository files navigation

Leaflet Theme Control

A Leaflet control for switching between visual themes using CSS filters. Perfect for adding dark mode, grayscale, and custom visual modes to your maps without requiring multiple tile layers.

Leaflet Theme Control Screenshot

Features

  • Multiple themes: Light, Dark, Grayscale, Custom
  • Theme Editor: Customize filters with live preview sliders (optional)
  • Accessibility: Adaptable themes for better visibility
  • CSS Filters: No need for multiple tile sources
  • Persistent: Saves user preference in localStorage
  • System Detection: Automatically detects OS dark mode preference
  • i18n Ready: Customizable labels with auto-update on language change
  • Lightweight: Zero dependencies (except Leaflet)
  • Performance: Instant theme switching without reloading tiles

Installation

As npm package

npm install leaflet-theme-control

With bundler (Webpack, Vite, Rollup):

import { ThemeControl } from "leaflet-theme-control";
import "leaflet-theme-control/src/leaflet-theme-control.css";

Without bundler (plain HTML):

<link rel="stylesheet" href="https://unpkg.com/leaflet-theme-control/src/leaflet-theme-control.css" />

<script type="importmap">
  {
    "imports": {
      "leaflet": "https://unpkg.com/leaflet@2.0.0-alpha.1/dist/leaflet.js",
      "leaflet-theme-control": "https://unpkg.com/leaflet-theme-control/src/leaflet-theme-control.js"
    }
  }
</script>

<script type="module">
  import { ThemeControl } from "leaflet-theme-control";
  // Your code here
</script>

Usage

Basic Example

import L from "leaflet";
import { ThemeControl } from "leaflet-theme-control";

const map = L.map("map").setView([51.505, -0.09], 13);

L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
  attribution: "Β© OpenStreetMap contributors"
}).addTo(map);

// Add theme control
new ThemeControl().addTo(map);

With Custom Options

new ThemeControl({
  position: "topright",
  defaultTheme: "light",
  detectSystemTheme: true,
  storageKey: "my-map-theme",

  // Custom label function for i18n
  getLabel: (themeKey) => {
    return i18n.t(`themes.${themeKey}`);
  },

  // Callback when theme changes
  onChange: (themeKey, theme) => {
    console.log(`Theme changed to: ${themeKey}`);
  }
}).addTo(map);

Custom Themes

import { ThemeControl } from "leaflet-theme-control";

new ThemeControl({
  themes: {
    light: {
      label: "Light Mode",
      filter: "",
      icon: "β˜€οΈ",
      controlStyle: "light",
      className: "theme-light"
    },
    dark: {
      label: "Dark Mode",
      filter: "invert(1) hue-rotate(180deg) saturate(0.6) brightness(0.5)",
      icon: "πŸŒ™",
      controlStyle: "dark",
      className: "theme-dark",
      applyToSelectors: [".my-sidebar", ".my-header"] // Apply filter to these elements too
    },
    monochrome: {
      label: "Black & White",
      filter: "grayscale(1) contrast(1.2)",
      icon: "⚫",
      controlStyle: "light",
      className: "theme-mono",
      applyToSelectors: ".my-sidebar" // Single selector also works
    },
    custom: {
      label: "My Theme",
      filter: "invert(1) hue-rotate(180deg) saturate(1) brightness(1) contrast(1) sepia(0.5) grayscale(0.5)",
      icon: "🎨",
      controlStyle: "dark",
      className: "theme-custom",
      applyToSelectors: [".my-sidebar", ".my-footer"]
    }
  }
}).addTo(map);

Theme Properties:

  • filter: CSS filter string (applied to map and applyToSelectors)
  • controlStyle: "light" or "dark" for Leaflet controls styling
  • className: CSS class added to <html> element (for custom styling)
  • applyToSelectors: String or Array of CSS selectors to apply the same filter to

Use Cases:

  • applyToSelectors: Apply the same dark mode filter to sidebar, header, footer etc.
  • className: Style elements differently per theme with CSS
/* Using className for custom styling */
.theme-dark .my-button {
  background: #2d2d2d;
  color: #e0e0e0;
}

/* Elements in applyToSelectors get the filter automatically */
.my-sidebar {
  background: white; /* Will be inverted in dark mode */
}

Programmatic Control (No UI Button)

For advanced use cases where you want to control themes from your own UI:

// Create control without visible button
const themeControl = new ThemeControl({
  addButton: false, // No UI button
  enableEditor: true, // Editor still available programmatically
  onChange: (theme) => {
    console.log("Theme changed:", theme);
  }
});

map.addControl(themeControl);

// Control themes programmatically
themeControl.setTheme("dark");
console.log(themeControl.getCurrentTheme()); // "dark"

// Open editor from custom button
myCustomButton.onclick = () => {
  themeControl.editor.openThemeSelector();
};

See examples/api.html for a complete example.

API

Options

Option Type Default Description
position String "topright" Position of the control
themes Object DEFAULT_THEMES Theme definitions
defaultTheme String "light" Initial theme
storageKey String "leaflet-theme" localStorage key
detectSystemTheme Boolean true Detect OS dark mode
cssSelector String ".leaflet-tile-pane" Elements to apply filter to
addButton Boolean true Add UI button to map (set to false for programmatic control only)
enableEditor Boolean false Enable theme editor UI with customization sliders
onChange Function null Callback on theme change AND editor changes: (themeKey, theme) => {}
getLabel Function null Function to get translated theme labels: (themeKey) => string (optional if themes have label property)
getEditorLabels Function null Function to get translated editor UI labels: (key) => string
panelPosition String "topright" Position of editor panel: "topright", "topleft", "bottomright", "bottomleft"
panelZIndex Number 1000 Z-index for editor panel to avoid conflicts

Methods

Method Returns Description
setTheme(themeKey) void Switch to specific theme
getCurrentTheme() String Get current theme key
getThemes() Object Get all available themes
updateButtonLabel() void Update button label (auto-called on html[lang])

Editor API (when enableEditor: true)

Method Returns Description
editor.openThemeSelector() void Open theme selector panel
editor.openThemeEditor(themeKey) void Open editor for specific theme
editor.close() void Close editor panel

Built-in Themes

  • Light: Default, no filter
  • Dark: Inverted colors with adjusted hue, saturation, and brightness
  • Grayscale: Black and white for printing or reduced distraction
  • Custom: Fully customizable theme with combined filters (editable via theme editor)

License

MIT License. See LICENSE for details.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Credits

Originally developed for the Veggiekarte project. But hopefully useful for others too!

About

A Leaflet control for switching between visual themes using CSS filters.

Topics

Resources

License

Stars

Watchers

Forks