.claude/skills/mapbox-gl-js/SKILL.md
Mapbox GL JS v3 development patterns for real estate data visualization. Use when working with Mapbox maps, Standard Style configuration, layer slots, 3D lighting, expressions, react-map-gl/mapbox integration, or migrating from MapLibre GL JS.
npx skillsauth add sdn0303/terrasight mapbox-gl-jsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Mapbox GL JS v3 patterns for the Terrasight real estate investment platform.
Load references/api-patterns.md and references/react-integration.md for
detailed code examples beyond what this summary covers.
@types/mapbox-gl)react-map-gl/mapbox endpoint for Mapbox GL JSThis project currently uses MapLibre GL JS via react-map-gl/maplibre.
Switching to Mapbox GL JS provides:
| Feature | MapLibre | Mapbox GL JS v3 |
|---------|----------|-----------------|
| License | MIT (free) | Proprietary (pay per map load) |
| 3D lighting presets | Manual | Built-in (Day/Dusk/Dawn/Night) |
| Standard Style | N/A | Auto-updating basemap |
| Layer slots | N/A | bottom / middle / top |
| 3D landmarks | N/A | Built-in |
| Globe projection | Community | Native with fog/atmosphere |
| Terrain shadows | N/A | Built-in |
Decision: Use MapLibre for cost-free development; switch to Mapbox when 3D lighting, Standard Style, or premium tile quality justifies the cost.
pnpm add mapbox-gl
# react-map-gl v8+ required for Mapbox GL JS v3.5+
pnpm add react-map-gl
// .env.local
NEXT_PUBLIC_MAPBOX_TOKEN=pk.xxx
// lib/mapbox.ts
import mapboxgl from 'mapbox-gl';
mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN!;
import Map from 'react-map-gl/mapbox';
import 'mapbox-gl/dist/mapbox-gl.css';
function PropertyMap() {
return (
<Map
mapboxAccessToken={process.env.NEXT_PUBLIC_MAPBOX_TOKEN}
initialViewState={{
longitude: 139.6917,
latitude: 35.6895,
zoom: 11,
pitch: 45,
}}
mapStyle="mapbox://styles/mapbox/standard"
/>
);
}
The Standard Style auto-updates with no migration required. Configure via
setConfigProperty after style.load:
import { useMap } from 'react-map-gl/mapbox';
function MapControls() {
const { current: map } = useMap();
const setLightPreset = (preset: 'day' | 'dusk' | 'dawn' | 'night') => {
map?.getMap().setConfigProperty('basemap', 'lightPreset', preset);
};
const togglePOI = (show: boolean) => {
map?.getMap().setConfigProperty('basemap', 'showPointOfInterestLabels', show);
};
const setTheme = (theme: 'default' | 'faded' | 'monochrome') => {
map?.getMap().setConfigProperty('basemap', 'theme', theme);
};
}
| Property | Type | Description |
|----------|------|-------------|
| showPlaceLabels | bool | Place label layers |
| showRoadLabels | bool | Road labels + shields |
| showPointOfInterestLabels | bool | POI icons and text |
| showTransitLabels | bool | Transit icons and text |
| show3dObjects | bool | 3D buildings, landmarks, trees, shadows |
| theme | string | default / faded / monochrome |
| lightPreset | string | day / dusk / dawn / night |
| font | string | Font family from predefined options |
Custom layers insert into predetermined positions in the Standard basemap:
| Slot | Position |
|------|----------|
| bottom | Above polygons (land, water) |
| middle | Above roads, behind 3D buildings |
| top | Above POI labels, behind place/transit labels |
| (none) | Above all existing layers |
map.addLayer({
id: 'transaction-circles',
type: 'circle',
slot: 'middle',
source: 'transactions',
paint: {
'circle-radius': ['interpolate', ['linear'], ['zoom'], 10, 3, 15, 8],
'circle-color': [
'interpolate', ['linear'], ['get', 'pricePerSqm'],
100000, '#2196F3',
500000, '#FF9800',
1000000, '#F44336',
],
'circle-emissive-strength': 0.8,
},
});
map.addSource('transactions', {
type: 'geojson',
data: featureCollection,
cluster: true,
clusterMaxZoom: 14,
clusterRadius: 50,
});
map.addSource('zoning', {
type: 'vector',
url: 'mapbox://username.tileset-id',
});
map.addLayer({
id: 'zoning-fill',
type: 'fill',
source: 'zoning',
'source-layer': 'zoning_areas',
paint: { 'fill-color': '#088', 'fill-opacity': 0.5 },
});
map.addSource('terrain-dem', {
type: 'raster-dem',
url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
});
map.setTerrain({ source: 'terrain-dem', exaggeration: 1.5 });
{
id: 'transactions',
type: 'circle',
slot: 'middle',
source: 'transactions',
paint: {
'circle-radius': ['interpolate', ['linear'], ['zoom'], 10, 2, 16, 10],
'circle-color': ['match', ['get', 'useType'],
'residential', '#4CAF50',
'commercial', '#2196F3',
'industrial', '#FF9800',
'#999',
],
'circle-opacity': 0.8,
'circle-emissive-strength': 1,
},
}
{
id: 'disaster-risk-3d',
type: 'fill-extrusion',
slot: 'middle',
source: 'risk-zones',
paint: {
'fill-extrusion-color': [
'interpolate', ['linear'], ['get', 'riskScore'],
0, '#4CAF50',
50, '#FF9800',
100, '#F44336',
],
'fill-extrusion-height': ['*', ['get', 'riskScore'], 100],
'fill-extrusion-opacity': 0.7,
'fill-extrusion-ambient-occlusion-ground-radius': 5,
'fill-extrusion-flood-light-intensity': 0.3,
},
}
{
id: 'price-heatmap',
type: 'heatmap',
slot: 'bottom',
source: 'transactions',
paint: {
'heatmap-weight': ['interpolate', ['linear'], ['get', 'pricePerSqm'],
0, 0, 1000000, 1,
],
'heatmap-intensity': ['interpolate', ['linear'], ['zoom'], 0, 1, 15, 3],
'heatmap-radius': ['interpolate', ['linear'], ['zoom'], 0, 2, 15, 20],
'heatmap-color': [
'interpolate', ['linear'], ['heatmap-density'],
0, 'rgba(33,102,172,0)',
0.2, 'rgb(103,169,207)',
0.4, 'rgb(209,229,240)',
0.6, 'rgb(253,219,199)',
0.8, 'rgb(239,138,98)',
1, 'rgb(178,24,43)',
],
},
}
{
id: 'facilities',
type: 'symbol',
slot: 'top',
source: 'facilities',
layout: {
'icon-image': ['get', 'icon'],
'icon-size': 0.8,
'text-field': ['get', 'name'],
'text-size': 11,
'text-offset': [0, 1.2],
'text-anchor': 'top',
},
paint: {
'text-color': '#333',
'text-halo-color': '#fff',
'text-halo-width': 1,
'text-emissive-strength': 1,
'icon-emissive-strength': 1,
},
}
// Match (categorical)
['match', ['get', 'category'], 'A', '#f00', 'B', '#0f0', '#999']
// Interpolate (continuous)
['interpolate', ['linear'], ['get', 'value'], 0, '#blue', 100, '#red']
// Step (discrete)
['step', ['get', 'value'], '#green', 50, '#yellow', 80, '#red']
// Case (conditional)
['case',
['<', ['get', 'price'], 100000], '#4CAF50',
['<', ['get', 'price'], 500000], '#FF9800',
'#F44336',
]
['interpolate', ['exponential', 1.5], ['zoom'],
10, 2, // zoom 10 → radius 2
15, 12, // zoom 15 → radius 12
20, 30, // zoom 20 → radius 30
]
// Random value per feature
['random']
// HSL color
['hsl', 200, 80, 50]
// Distance from geometry (meters)
['distance', { type: 'Point', coordinates: [139.69, 35.68] }]
// Config value (Standard Style)
['config', 'lightPreset']
map.setLights([
{
id: 'ambient',
type: 'ambient',
properties: { color: 'white', intensity: 0.4 },
},
{
id: 'directional',
type: 'directional',
properties: {
color: 'white',
intensity: 0.8,
direction: [200, 40],
'cast-shadows': true,
'shadow-intensity': 0.3,
},
},
]);
// Mapbox GL JS v3.5+
import Map, { Source, Layer, Marker, Popup } from 'react-map-gl/mapbox';
import 'mapbox-gl/dist/mapbox-gl.css';
// MapLibre (current project setup)
import Map, { Source, Layer, Marker, Popup } from 'react-map-gl/maplibre';
import 'maplibre-gl/dist/maplibre-gl.css';
import Map, { type MapLayerMouseEvent } from 'react-map-gl/mapbox';
function PropertyMap() {
const handleClick = useCallback((e: MapLayerMouseEvent) => {
const feature = e.features?.[0];
if (!feature) return;
setSelectedProperty(feature.properties as TransactionProperty);
}, []);
const handleMouseEnter = useCallback(() => {
setCursor('pointer');
}, []);
return (
<Map
mapboxAccessToken={token}
interactiveLayerIds={['transaction-circles']}
onClick={handleClick}
onMouseEnter={handleMouseEnter}
onMouseLeave={() => setCursor('')}
cursor={cursor}
/>
);
}
import { useMapStore } from '@/stores/mapStore';
function SyncedMap() {
const viewState = useMapStore((s) => s.viewState);
const setViewState = useMapStore((s) => s.setViewState);
return (
<Map
{...viewState}
onMove={(e) => setViewState(e.viewState)}
mapboxAccessToken={token}
mapStyle="mapbox://styles/mapbox/standard"
/>
);
}
pnpm remove maplibre-gl
pnpm add mapbox-gl
// Before
import Map from 'react-map-gl/maplibre';
import 'maplibre-gl/dist/maplibre-gl.css';
// After
import Map from 'react-map-gl/mapbox';
import 'mapbox-gl/dist/mapbox-gl.css';
// Before (MapLibre — free tile provider)
mapStyle="https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json"
// After (Mapbox Standard — auto-updating)
mapStyle="mapbox://styles/mapbox/standard"
<Map mapboxAccessToken={process.env.NEXT_PUBLIC_MAPBOX_TOKEN} />
slot property to custom layerssetConfigProperty*-emissive-strength paint properties for lightingcluster: true for point data at low zoom levelsmapbox://) for datasets > 10,000 featuresfill-extrusion is GPU-intensive — limit to essential layersstyle.load event over load for adding layersnew Map() instantiation)[longitude, latitude] order alwaysproperties: must be a JSON object (not null)geometry(Point, 4326) for WGS84 in PostGISdevelopment
Rust coding rules for Axum/Tokio/SQLx backends in services/backend. 179 rules split into 14 category files covering ownership, error handling, async, API design, and more. Use when writing, reviewing, or refactoring Rust code, designing error types, async flows, or public APIs.
content-media
PostgreSQL and PostGIS patterns for schema design, spatial queries, query optimization, indexing, and zero-downtime migrations. Use when writing SQL, creating tables, optimizing queries, or writing migration files.
development
MapLibre GL JS + PostGIS integration patterns for real estate data visualization. Use when working on map layers, spatial queries, GeoJSON data pipelines, or 3D extrusion effects.
development
Frontend development rules for Next.js 16, React 19, TanStack Query, Zustand, Zod, Tailwind v4, and shadcn/ui in services/frontend. Use when writing, reviewing, or refactoring React components, data fetching hooks, state management, forms, or UI styling.