A Vue 3 component for comparing MapLibre maps side-by-side with a draggable slider. Perfect for visualizing differences between map styles or showing before/after comparisons.
- Side-by-side map comparison with smooth dragging slider
- Support for different MapLibre styles on each side, or same style with different layers
- Layer visibility control for both maps
- Touch and mouse support
- Built with Vue 3 and TypeScript using
defineComponent - Synchronized map movements (pan, zoom, rotate, pitch)
- Two main components:
MapCompare(supports both different styles and same style with different layers), andToggleCompare(toggleable comparison mode)
Comparing two different map styles side-by-side with customizable swiper options
npm install vue-maplibre-compare maplibre-glimport { createApp } from 'vue'
import MapComparePlugin from 'vue-maplibre-compare'
import 'vue-maplibre-compare/dist/vue-maplibre-compare.css'
import App from './App.vue'
const app = createApp(App)
app.use(MapComparePlugin)
app.mount('#app')
// Now you can use the components in your templates:
// <MapCompare> and <ToggleCompare>You can also import individual components:
// Import specific components
import { MapCompare, ToggleCompare } from 'vue-maplibre-compare'
import 'vue-maplibre-compare/dist/vue-maplibre-compare.css'
// Or import all at once
import * as VueMaplibreCompare from 'vue-maplibre-compare'<template>
<div style="width: 100%; height: 600px;">
<!-- Compare two different map styles -->
<MapCompare
:map-style-a="styleA"
:map-style-b="styleB"
:map-layers-a="layersA"
:map-layers-b="layersB"
:camera="{ center: [-74.5, 40], zoom: 9 }"
/>
<!-- Or compare different layers from the same style (mapStyleB is optional) -->
<MapCompare
:map-style-a="styleA"
:map-layers-a="layersA"
:map-layers-b="layersB"
:camera="{ center: [-74.5, 40], zoom: 9 }"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { MapCompare } from 'vue-maplibre-compare'
const styleA = 'https://demotiles.maplibre.org/style.json'
const styleB = 'https://demotiles.maplibre.org/style.json'
// Optional: specify which layers to show on each map
// If empty or not provided, all layers are shown
const layersA = ref(['water', 'roads', 'buildings'])
const layersB = ref(['water', 'buildings']) // roads hidden on map B
</script>You can also pass MapLibre StyleSpecification objects directly:
<script setup lang="ts">
import { MapCompare } from 'vue-maplibre-compare'
import type { StyleSpecification } from 'maplibre-gl'
const blueStyle: StyleSpecification = {
version: 8,
sources: {},
layers: [
{
id: 'background',
type: 'background',
paint: { 'background-color': '#1e3a8a' }
}
]
}
const redStyle: StyleSpecification = {
version: 8,
sources: {},
layers: [
{
id: 'background',
type: 'background',
paint: { 'background-color': '#991b1b' }
}
]
}
</script>
<template>
<MapCompare
:mapStyleA="blueStyle"
:mapStyleB="redStyle"
:camera="{ center: [-74.5, 40], zoom: 9 }"
/>
</template>The MapCompare component supports comparing different layers from the same map style by omitting the mapStyleB prop. When mapStyleB is not provided, it defaults to mapStyleA:
<template>
<div style="width: 100%; height: 600px;">
<MapCompare
:map-style-a="'https://demotiles.maplibre.org/style.json'"
:map-layers-a="['water', 'roads']"
:map-layers-b="['water', 'buildings', 'parks']"
:camera="{ center: [-74.5, 40], zoom: 9 }"
/>
</div>
</template>
<script setup lang="ts">
import { MapCompare } from 'vue-maplibre-compare'
import 'vue-maplibre-compare/dist/vue-maplibre-compare.css'
</script>This library provides two main components for map comparison. Here's when to use each:
- Always-on comparison: You want to display two maps side-by-side at all times
- Static comparison views: The comparison state doesn't need to change dynamically
- Simpler implementation: You don't need toggle functionality or single-map mode
- Performance: You want a lighter component that only handles comparison mode
Common use cases:
- Before/after visualizations (e.g., showing map changes over time)
- Style comparison pages (comparing different map styles)
- Layer visibility comparisons (showing different layer combinations)
- Documentation or demo pages with fixed comparison views
- Dynamic toggling: Users need to switch between single-map and comparison modes
- Optional comparison: Comparison is a feature that can be enabled/disabled
- Space optimization: You want to show a full-width map by default, with comparison as an option
- User-controlled views: Users should control when to compare maps
- Map instance access: You need direct access to map instances via
map-ready-aandmap-ready-bevents
Common use cases:
- Interactive map applications with a "Compare" toggle button
- Map viewers where comparison is an optional feature
- Applications where users can choose to view one map or compare two
- Scenarios requiring programmatic access to map instances
| Requirement | Component |
|---|---|
| Always show two maps side-by-side | MapCompare |
| Need to toggle between single map and comparison | ToggleCompare |
| Simple, static comparison view | MapCompare |
| User-controlled comparison feature | ToggleCompare |
Need map instance events (map-ready-a, map-ready-b) |
ToggleCompare |
| Fixed comparison layout | MapCompare |
Compare two different map styles side-by-side, or compare different layers from the same style.
Props:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
mapStyleA |
string | StyleSpecification |
Yes | - | MapLibre style for left/first map |
mapStyleB |
string | StyleSpecification |
No | mapStyleA |
MapLibre style for right/second map. If not provided, uses mapStyleA (useful for comparing different layers from the same style) |
mapLayersA |
string[] |
No | [] |
Array of layer IDs to enable in map A. If empty, all layers are shown |
mapLayersB |
string[] |
No | [] |
Array of layer IDs to enable in map B. If empty, all layers are shown |
camera |
{ center: [number, number], zoom: number, pitch: number, bearing: number} |
Yes | { center: [-74.5, 40], zoom: 9, pitch: 0, bearing: 0 } |
Camera Location/Orientation settings |
layerOrder |
'topmost' or 'bottommost' |
No | 'topmost' |
Determines in what order layers should be rendered, topmost has the first layer as the highest in the stack, bottommost has the bottom layer as the highest in stack order |
transformRequest |
function Docs |
No | undefined |
transform invidual requests |
headers |
Record<string, any> |
No | {} |
Headers to add to requests sent by the map NOTE: this will add to every map request, use transformRequest for more control |
attributionControl |
AttributionControlOptions | false |
No | undefined |
Attribution control options or false to disable |
swiperOptions |
SwiperOptions |
No | default |
Configuration object for the swiper appearance and behavior |
Emits:
| Event | Payload | Description |
|---|---|---|
panend |
{ center: [number, number], zoom: number, bearing: number, pitch: number } |
Emitted when the user finishes panning the map |
zoomend |
{ center: [number, number], zoom: number, bearing: number, pitch: number } |
Emitted when the user finishes zooming the map |
pitchend |
{ center: [number, number], zoom: number, bearing: number, pitch: number } |
Emitted when the user finishes changing the map pitch (tilt) |
rotateend |
{ center: [number, number], zoom: number, bearing: number, pitch: number } |
Emitted when the user finishes rotating the map |
loading-complete |
void |
Emitted when both maps have finished loading and are ready |
sliderend |
{ percentage: number, position: number } |
Emitted when the user finishes dragging the slider. percentage is 0-100 representing how much of mapA is visible (0% = all mapB, 100% = all mapA). position is the pixel position of the slider. |
interface SwiperOptions {
thickness?: number; // Width of the visible line (default: 4)
orientation: 'vertical' | 'horizontal'; // Orientation of the slider
grabThickness?: number; // Width of the interactive area (default: 20)
handleSize?: number; // Diameter of the handle circle (default: 40)
lineColor?: string; // Color of the divider line (default: 'white')
handleColor?: string; // Color of the handle circle (default: 'white')
handleShadowColor?: string; // Color of the handle shadow
arrowColor?: string; // Color of the arrows
darkMode?: boolean // Swaps to darkmode if other colors aren't provided
}The component now includes built-in support for the pmtiles protocol. You can use pmtiles:// URLs directly in your style sources.
A hybrid component that can work in single-map mode or comparison mode. Perfect for scenarios where you want to toggle between showing one map or comparing two maps side-by-side.
Props:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
mapStyleA |
string | StyleSpecification |
No | OpenStreetMap style | MapLibre style for the primary/left map |
mapStyleB |
string | StyleSpecification |
No | undefined |
MapLibre style for the secondary/right map (only used when compareEnabled is true) |
mapLayersA |
string[] |
No | [] |
Array of layer IDs to enable in map A. If empty, all layers are shown |
mapLayersB |
string[] |
No | [] |
Array of layer IDs to enable in map B. If empty, all layers are shown |
camera |
{ center: [number, number], zoom: number, pitch: number, bearing: number} |
No | { center: [0, 0], zoom: 1, pitch: 0, bearing: 0 } |
Camera Location/Orientation settings |
compareEnabled |
boolean |
No | false |
Whether comparison mode is enabled. When false, shows only mapStyleA |
layerOrder |
'topmost' or 'bottommost' |
No | 'topmost' |
Determines in what order layers should be rendered |
transformRequest |
function |
No | undefined |
Transform individual requests |
headers |
Record<string, string> |
No | {} |
Headers to add to requests sent by the map |
mapContainerId |
string |
No | 'mapContainer' |
ID for the map container element |
attributionControl |
AttributionControlOptions | false |
No | undefined |
Attribution control options or false to disable |
swiperOptions |
SwiperOptions |
No | default |
Configuration object for the swiper appearance and behavior |
Emits:
| Event | Payload | Description |
|---|---|---|
panend |
{ center: [number, number], zoom: number, bearing: number, pitch: number } |
Emitted when the user finishes panning the map |
zoomend |
{ center: [number, number], zoom: number, bearing: number, pitch: number } |
Emitted when the user finishes zooming the map |
pitchend |
{ center: [number, number], zoom: number, bearing: number, pitch: number } |
Emitted when the user finishes changing the map pitch (tilt) |
rotateend |
{ center: [number, number], zoom: number, bearing: number, pitch: number } |
Emitted when the user finishes rotating the map |
loading-complete |
void |
Emitted when the map(s) have finished loading and are ready |
sliderend |
{ percentage: number, position: number } |
Emitted when the user finishes dragging the slider. percentage is 0-100 representing how much of mapA is visible (0% = all mapB, 100% = all mapA). position is the pixel position of the slider. |
map-ready-a |
MaplibreMap |
Emitted when map A is ready, provides the map instance |
map-ready-b |
MaplibreMap |
Emitted when map B is ready, provides the map instance (only in compare mode) |
Usage:
<template>
<div style="width: 100%; height: 600px;">
<ToggleCompare
:mapStyleA="primaryStyle"
:mapStyleB="secondaryStyle"
:compareEnabled="isComparing"
:camera="{ center: [-74.5, 40], zoom: 9 }"
@map-ready-a="onMapAReady"
@map-ready-b="onMapBReady"
/>
<button @click="isComparing = !isComparing">
{{ isComparing ? 'Single Map' : 'Compare Maps' }}
</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ToggleCompare } from 'vue-maplibre-compare'
import type { Map as MaplibreMap } from 'maplibre-gl'
const isComparing = ref(false)
const primaryStyle = 'https://demotiles.maplibre.org/style.json'
const secondaryStyle = 'https://example.com/other-style.json'
const onMapAReady = (map: MaplibreMap) => {
console.log('Map A ready:', map)
}
const onMapBReady = (map: MaplibreMap) => {
console.log('Map B ready:', map)
}
</script>git clone <repository-url>
cd vue-maplibre-compare
npm installnpm run devThis will start a development server at http://localhost:3000 with a demo application showing the component in action.
npm run buildThis will create the distributable library files in the dist directory.
This project uses semantic versioning with standard-version. See VERSIONING.md for detailed information about:
- How to bump versions locally
- Publishing via GitHub Actions workflow
- Commit message conventions
- Best practices
Quick version commands:
npm run version # Auto-detect version bump from commits
npm run version:patch # Bump patch version (1.0.0 -> 1.0.1)
npm run version:minor # Bump minor version (1.0.0 -> 1.1.0)
npm run version:major # Bump major version (1.0.0 -> 2.0.0)