A TypeScript control for switching Mapbox GL / MapLibre GL map styles. Easily add a floating style switcher to your map app, with support for multiple styles, images, dark/light themes, and before/after change callbacks.
Available as:
StyleSwitcherControl- Direct IControl implementation for Mapbox/MapLibre GLMapGLStyleSwitcher- React component for react-map-gl integrationuseStyleSwitcher- React hook for custom map instances
- IControl implementation for Mapbox GL / MapLibre GL
- React component for react-map-gl integration
- React hook for custom map instances
- Floating style switcher in any corner (via map.addControl position)
- Support for multiple map styles with thumbnails
- Expand/collapse on hover with smooth animations
- Dark/light/auto theme support —
theme: 'auto'reacts to OS preference changes in real time - RTL text support for Arabic/Hebrew interfaces
- Configurable display options (show/hide labels and images)
- Callbacks for before/after style change
updateOptions()for imperative option updates after mount- Fully customizable CSS classes
- TypeScript support
- Accessibility features (ARIA labels, keyboard navigation)
- ESM-only package (tree-shakeable, no CommonJS bundle)
- Comprehensive test coverage
# Using npm (recommended)
npm install map-gl-style-switcher
# Using yarn
yarn add map-gl-style-switcher
# Using pnpm
pnpm add map-gl-style-switcherNote: This package ships ES modules only. CommonJS
require()is not supported. All modern bundlers (Vite, webpack 5, Rollup, esbuild) handle ESM natively.
import { StyleSwitcherControl, type StyleItem } from 'map-gl-style-switcher';
import 'map-gl-style-switcher/dist/map-gl-style-switcher.css';
const styles: StyleItem[] = [
{
id: 'voyager',
name: 'Voyager',
image:
'https://raw.githubusercontent.com/muimsd/map-gl-style-switcher/refs/heads/main/public/voyager.png',
styleUrl: 'https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json',
description: 'Voyager style from Carto',
},
{
id: 'positron',
name: 'Positron',
image:
'https://raw.githubusercontent.com/muimsd/map-gl-style-switcher/refs/heads/main/public/positron.png',
styleUrl: 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json',
description: 'Positron style from Carto',
},
];
const control = new StyleSwitcherControl({
styles,
activeStyleId: 'voyager',
theme: 'auto',
onAfterStyleChange: (_from, to) => {
map.setStyle(to.styleUrl);
},
});
map.addControl(control, 'bottom-left');Use this when you manage the map instance yourself (e.g., inside useEffect):
import { useEffect, useRef } from 'react';
import maplibregl from 'maplibre-gl';
import { useStyleSwitcher, type StyleItem } from 'map-gl-style-switcher/react';
import 'maplibre-gl/dist/maplibre-gl.css';
import 'map-gl-style-switcher/dist/map-gl-style-switcher.css';
const styles: StyleItem[] = [
/* ... */
];
function MapComponent() {
const mapContainer = useRef<HTMLDivElement>(null);
const mapRef = useRef<maplibregl.Map | null>(null);
useEffect(() => {
if (!mapContainer.current) return;
mapRef.current = new maplibregl.Map({
container: mapContainer.current,
style: styles[0].styleUrl,
center: [0, 0],
zoom: 2,
});
return () => mapRef.current?.remove();
}, []);
useStyleSwitcher(mapRef.current, {
styles,
theme: 'auto',
position: 'bottom-left',
onAfterStyleChange: (_from, to) => {
mapRef.current?.setStyle(to.styleUrl);
},
});
return <div ref={mapContainer} style={{ width: '100%', height: '500px' }} />;
}Tip: The hook recreates the control whenever options change (deep comparison via JSON serialisation). Pass stable object references or memoize options if performance is critical.
Use this when your map is rendered by react-map-gl:
import { useState } from 'react';
import { Map } from 'react-map-gl/maplibre';
import { MapGLStyleSwitcher, type StyleItem } from 'map-gl-style-switcher/react-map-gl';
import 'maplibre-gl/dist/maplibre-gl.css';
import 'map-gl-style-switcher/dist/map-gl-style-switcher.css';
const styles: StyleItem[] = [
/* ... */
];
function MyMap() {
const [mapStyle, setMapStyle] = useState(styles[0].styleUrl);
const [activeStyleId, setActiveStyleId] = useState(styles[0].id);
const handleStyleChange = (styleUrl: string) => {
setMapStyle(styleUrl);
const style = styles.find(s => s.styleUrl === styleUrl);
if (style) setActiveStyleId(style.id);
};
return (
<Map mapStyle={mapStyle} initialViewState={{ longitude: 0, latitude: 0, zoom: 2 }}>
<MapGLStyleSwitcher
styles={styles}
activeStyleId={activeStyleId}
theme="auto"
position="bottom-left"
onStyleChange={handleStyleChange}
/>
</Map>
);
}MapGLStyleSwitcher propagates all non-callback prop changes to the underlying control after mount — changing activeStyleId, theme, showLabels, styles, etc. is fully reactive. Callbacks (onBeforeStyleChange, onAfterStyleChange, onStyleChange) are always up-to-date via refs and do not cause the control to be recreated.
View the react-map-gl example →
interface StyleSwitcherControlOptions {
styles: StyleItem[]; // Array of map styles (required)
activeStyleId?: string; // Active style ID (default: first style)
onBeforeStyleChange?: (from: StyleItem, to: StyleItem) => void; // Called before style changes
onAfterStyleChange?: (from: StyleItem, to: StyleItem) => void; // Called after style changes
showLabels?: boolean; // Show style names (default: true)
showImages?: boolean; // Show thumbnails (default: true)
animationDuration?: number; // Expand animation in ms (default: 200)
maxHeight?: number; // Max list height in px (default: 300)
theme?: 'light' | 'dark' | 'auto'; // UI theme (default: 'light')
design?: 'default' | 'outlined'; // Visual design variant (default: 'default')
classNames?: Partial<StyleSwitcherClassNames>; // Custom CSS class overrides
rtl?: boolean; // RTL layout (default: false)
}
interface StyleItem {
id: string; // Unique identifier
name: string; // Display name
image: string; // Thumbnail URL or data URI
styleUrl: string; // MapLibre/Mapbox style URL
description?: string; // Optional tooltip text
}
interface StyleSwitcherClassNames {
container: string; // Main container class
list: string; // Expanded list container class
item: string; // Individual style item class
itemSelected: string; // Selected item class
itemHideLabel: string; // No-label utility class
dark: string; // Dark theme class
light: string; // Light theme class
outlined: string; // Outlined design variant class (applied when design is 'outlined')
}activeStyleId: Controls both the initially selected style and what is shown in the collapsed state. When usingMapGLStyleSwitcher, updating this prop reactively switches the highlighted style.showLabels&showImages: At least one must betrue(throws otherwise).theme:'light': Light colour scheme (default)'dark': Dark colour scheme'auto': Follows the OS preference and updates automatically when the user switches between light and dark mode
design:'default': Solid container with shadow (default)'outlined': Translucent backdrop with a visible border on the container and on each item — ideal when you want a lighter chrome over a busy map. The class applied for this variant is configurable viaclassNames.outlined.
rtl: Enables right-to-left layout for Arabic/Hebrew interfaces.
After adding the control to a map you can update its options without recreating it:
const control = new StyleSwitcherControl({ styles, activeStyleId: 'voyager' });
map.addControl(control, 'bottom-left');
// Later — switch active style programmatically
control.updateOptions({ activeStyleId: 'positron' });
// Or change multiple options at once
control.updateOptions({ theme: 'dark', showLabels: false });Only the keys you pass are updated; all others remain unchanged. If the control has already been added to the map, the UI re-renders immediately.
Override all CSS classes used by the control via the classNames option:
const control = new StyleSwitcherControl({
styles,
classNames: {
container: 'my-style-switcher',
list: 'my-style-list',
item: 'my-style-item',
itemSelected: 'my-style-item-selected',
itemHideLabel: 'my-style-item-hide-label',
dark: 'my-style-dark',
light: 'my-style-light',
outlined: 'my-style-outlined',
},
});We welcome contributions! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
-
Clone the repository
git clone https://github.com/muimsd/map-gl-style-switcher cd map-gl-style-switcher -
Install dependencies
npm install
-
Start development server
npm run dev
-
Make your changes
- Follow TypeScript best practices
- Maintain backward compatibility when possible
- Add tests for new features
-
Test your changes
npm run validate # Runs type-check, lint, format-check, and tests -
Submit a pull request
- Use npm for dependency management
- Follow TypeScript best practices
- Maintain backward compatibility when possible
- Add tests for new features
- Update documentation as needed
- Follow the existing code style
- Ensure all checks pass:
npm run validate
MIT License - see LICENSE file for details.
Made with ❤️ for the MapLibre GL and Mapbox GL community

