npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@classic-homes/maps-react

v0.2.12

Published

React MapLibre components for Classic Theme

Downloads

1,817

Readme

@classic-homes/maps-react

React components for MapLibre GL maps in the Classic Theme design system.

Installation

npm install @classic-homes/maps-react maplibre-gl

Features

  • Declarative Components: React-friendly API for MapLibre GL
  • 7 Layer Types: Fill, Line, Circle, Symbol, FillExtrusion, Heatmap, Raster
  • Controls: Navigation, Scale, Geolocation, Legend, Coordinate Display
  • Clustering: Built-in cluster support with expansion utilities
  • Deck.gl Integration: Optional overlay for advanced visualizations
  • Accessibility: ARIA labels, keyboard navigation, reduced motion support
  • Theme Integration: Light/dark themes from design tokens

Quick Start

import { Map, MapContainer, Source, FillLayer } from '@classic-homes/maps-react';
import 'maplibre-gl/dist/maplibre-gl.css';

function MyMap() {
  return (
    <MapContainer className="h-[500px]">
      <Map
        initialCenter={[-122.4, 37.8]}
        initialZoom={12}
        onClick={(e) => console.log('Clicked:', e.lngLat)}
      >
        <Source id="lots" data={geojsonData}>
          <FillLayer id="lots-fill" fillColor="#22c55e" fillOpacity={0.7} />
        </Source>
      </Map>
    </MapContainer>
  );
}

Components

Map Container

import { MapContainer, MapSkeleton, MapError, MapEmpty } from '@classic-homes/maps-react';

// Responsive container with loading states
<MapContainer className="h-[500px]">
  {isLoading && <MapSkeleton />}
  {error && <MapError message={error} onRetry={retry} />}
  {isEmpty && <MapEmpty message="No data available" />}
  {data && <Map>...</Map>}
</MapContainer>;

Core Map

import { Map, useMap, useMapInstance } from '@classic-homes/maps-react';

<Map
  // Initial viewport
  initialCenter={[-122.4, 37.8]}
  initialZoom={12}
  initialBearing={0}
  initialPitch={0}
  // Bounds fitting
  bounds={bounds}
  fitBoundsOptions={{ padding: 50 }}
  // Style
  style={lightBasicStyle}
  theme="light"
  // Interactivity
  interactive={true}
  scrollZoom={true}
  dragPan={true}
  // Events (see Callback Memoization section)
  onClick={handleClick}
  onHover={handleHover}
  onMove={handleMove}
  onLoad={handleLoad}
/>;

// Access map instance in child components
function MapChild() {
  const { map, loaded } = useMap();
  // or
  const map = useMapInstance();
}

Data Sources

import { Source } from '@classic-homes/maps-react';

// GeoJSON data
<Source id="features" data={geojsonFeatureCollection}>
  {/* Layers go here */}
</Source>

// URL source
<Source id="remote" data="https://example.com/data.geojson">
  {/* Layers go here */}
</Source>

// With clustering
<Source
  id="points"
  data={pointData}
  cluster={true}
  clusterRadius={50}
  clusterMaxZoom={14}
>
  {/* Layers go here */}
</Source>

Layer Components

import {
  FillLayer,
  LineLayer,
  CircleLayer,
  SymbolLayer,
  FillExtrusionLayer,
  HeatmapLayer,
} from '@classic-homes/maps-react';

// Fill layer (polygons)
<FillLayer
  id="polygons"
  source="features"
  fillColor="#22c55e"
  fillOpacity={0.7}
  fillOutlineColor="#166534"
  onClick={handleClick}
  onHover={handleHover}
/>

// Line layer
<LineLayer
  id="lines"
  source="features"
  lineColor="#3b82f6"
  lineWidth={2}
  lineCap="round"
/>

// Circle layer (points)
<CircleLayer
  id="points"
  source="features"
  circleRadius={6}
  circleColor="#ef4444"
  circleStrokeWidth={2}
  circleStrokeColor="#ffffff"
/>

// Symbol layer (labels/icons)
<SymbolLayer
  id="labels"
  source="features"
  textField={['get', 'name']}
  textSize={12}
  iconImage="marker"
/>

// 3D extrusions
<FillExtrusionLayer
  id="buildings"
  source="features"
  fillExtrusionHeight={['get', 'height']}
  fillExtrusionColor="#94a3b8"
  fillExtrusionOpacity={0.9}
/>

// Heatmap
<HeatmapLayer
  id="heat"
  source="features"
  heatmapWeight={1}
  heatmapIntensity={0.5}
  heatmapRadius={30}
/>

Markers and Popups

import { Marker, Popup, PopupCard, PopupHeader, PopupContent, PopupFooter } from '@classic-homes/maps-react';

// Custom marker with popup
<Marker
  lngLat={[-122.4, 37.8]}
  anchor="bottom"
  draggable={true}
  onClick={handleClick}
  onDragEnd={handleDrag}
>
  <div className="custom-marker">
    <MapPinIcon />
  </div>
</Marker>

// Popup with card styling
<Popup lngLat={[-122.4, 37.8]} anchor="bottom" closeOnClick={false}>
  <PopupCard>
    <PopupHeader>
      <h3>Location Name</h3>
    </PopupHeader>
    <PopupContent>
      <p>Description goes here</p>
    </PopupContent>
    <PopupFooter>
      <button>View Details</button>
    </PopupFooter>
  </PopupCard>
</Popup>

Controls

import { Legend, LegendGroup, CoordinateDisplay } from '@classic-homes/maps-react';

// Legend
<Legend
  position="bottom-left"
  title="Status"
  items={[
    { label: 'Available', color: '#22c55e' },
    { label: 'Pending', color: '#f59e0b' },
    { label: 'Sold', color: '#ef4444' },
  ]}
/>

// Legend group with collapsible sections
<LegendGroup
  position="bottom-left"
  legends={[
    {
      title: 'Status',
      items: [{ label: 'Available', color: '#22c55e' }],
    },
    {
      title: 'Price',
      gradient: {
        stops: [
          { value: 0, color: '#eff6ff', label: '$0' },
          { value: 100, color: '#1e40af', label: '$1M+' },
        ],
      },
    },
  ]}
/>

// Coordinate display
<CoordinateDisplay position="bottom-right" precision={4} />

Deck.gl Overlay

import { DeckOverlay } from '@classic-homes/maps-react';
import { ScatterplotLayer } from '@deck.gl/layers';

<Map>
  <DeckOverlay
    layers={[
      new ScatterplotLayer({
        id: 'scatter',
        data: points,
        getPosition: (d) => d.coordinates,
        getRadius: 100,
        getFillColor: [255, 0, 0],
      }),
    ]}
  />
</Map>;

Callback Memoization

Important: Event callbacks passed to map components should be memoized to prevent unnecessary effect re-runs. Unmemoized callbacks can cause event handlers to be detached and reattached on every render.

import { useCallback, useMemo } from 'react';

function MyMap() {
  // GOOD: Memoize callbacks
  const handleClick = useCallback((e: MapClickEventParams) => {
    console.log('Clicked:', e.lngLat);
  }, []);

  const handleHover = useCallback((e: MapHoverEventParams) => {
    setHoveredFeature(e.features?.[0] ?? null);
  }, []);

  // GOOD: Memoize layer event handlers
  const handleFeatureClick = useCallback((e: FeatureClickEventParams) => {
    setSelectedId(e.features[0]?.id);
  }, []);

  return (
    <Map onClick={handleClick} onHover={handleHover}>
      <Source id="lots" data={data}>
        <FillLayer id="lots-fill" fillColor="#22c55e" onClick={handleFeatureClick} />
      </Source>
    </Map>
  );
}

// BAD: Inline callbacks cause re-attachment
function BadExample() {
  return (
    <Map
      // This creates a new function every render!
      onClick={(e) => console.log(e)}
    >
      ...
    </Map>
  );
}

Hooks

useMapTheme

import { useMapTheme } from '@classic-homes/maps-react';

function ThemedMap() {
  const { theme, style, isDark, toggleTheme } = useMapTheme();

  return (
    <>
      <Map style={style} theme={theme} />
      <button onClick={toggleTheme}>{isDark ? 'Light Mode' : 'Dark Mode'}</button>
    </>
  );
}

useReducedMotion

import { useReducedMotion } from '@classic-homes/maps-react';

function AnimatedMap() {
  const prefersReducedMotion = useReducedMotion();

  const flyToLocation = (lngLat) => {
    map.flyTo({
      center: lngLat,
      duration: prefersReducedMotion ? 0 : 1000,
    });
  };
}

useLayer

import { useLayer } from '@classic-homes/maps-react';

// Create custom layer components
function CustomLayer({ id, source, ...props }) {
  const { map, loaded } = useMap();

  useLayer({
    map,
    loaded,
    id,
    source,
    type: 'fill',
    paint: {
      'fill-color': props.color,
    },
    onClick: props.onClick,
  });

  return null;
}

Cluster Utilities

import {
  isClusterFeature,
  expandCluster,
  getClusterBounds,
  expandClusterToBounds,
} from '@classic-homes/maps-react';

function handleClusterClick(e: FeatureClickEventParams) {
  const feature = e.features[0];

  if (isClusterFeature(feature)) {
    // Option 1: Zoom to expand
    expandCluster(map, 'source-id', feature.id, (zoom) => {
      map.flyTo({ center: e.lngLat, zoom });
    });

    // Option 2: Fit to cluster bounds
    expandClusterToBounds(map, 'source-id', feature, (bounds) => {
      map.fitBounds(bounds, { padding: 50 });
    });
  }
}

TypeScript

All components are fully typed:

import type {
  MapComponentProps,
  SourceComponentProps,
  FillLayerComponentProps,
  MapClickEventParams,
  FeatureClickEventParams,
  FeatureHoverEventParams,
} from '@classic-homes/maps-react';

Peer Dependencies

  • react >= 18.0.0
  • react-dom >= 18.0.0
  • maplibre-gl >= 4.0.0 || >= 5.0.0

License

MIT