A Leaflet control for switching between visual themes (light, dark, grayscale, custom, etc.) using CSS filters
npm install leaflet-theme-controlA 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
- 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
``bash`
npm install leaflet-theme-control
With bundler (Webpack, Vite, Rollup):
`javascript`
import { ThemeControl } from "leaflet-theme-control";
import "leaflet-theme-control/src/leaflet-theme-control.css";
Without bundler (plain HTML):
`html
`
`javascript
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);
`
`javascript
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);
`javascript
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 stylingclassName
- : CSS class added to 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
`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:
`javascript
// 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!