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.
- 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
npm install leaflet-theme-controlWith 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>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);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);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 andapplyToSelectors)controlStyle:"light"or"dark"for Leaflet controls stylingclassName: 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 */
}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.
| 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 |
| 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]) |
| 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 |
- 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)
MIT License. See LICENSE for details.
Contributions are welcome! Please feel free to submit a Pull Request.
Originally developed for the Veggiekarte project. But hopefully useful for others too!
