Build a Minimal Next.js Leaflet Order Map (Shopify-Style)

Create a desaturated, Shopify-style pickup map with Leaflet in Next.js App Router — client-only, performant, and…

·Updated on:·Matija Žiberna·
Build a Minimal Next.js Leaflet Order Map (Shopify-Style)

⚡ Next.js Implementation Guides

In-depth Next.js guides covering App Router, RSC, ISR, and deployment. Get code examples, optimization checklists, and prompts to accelerate development.

No spam. Unsubscribe anytime.

When building custom e-commerce checkout flows, the standard OpenStreetMap look—busy, colorful, and chaotic—often clashes with a clean brand identity. Developers often aim for that polished "Shopify experience": a desaturated, reassuring map that clearly indicates a pickup point without distracting from the primary goal (confirmation).

Defining "Shopify-Style"

Before we write code, let’s define exactly what we mean by "Shopify-style" in this context. We are not replicating their entire reliability infrastructure or analytics stack.

Instead, we are replicating their specific UX pattern:

  1. Desaturated Tiles: High-contrast, monochromatic basemaps that recede into the background.
  2. Minimal Interaction: The map is strictly for visual confirmation, not exploration.
  3. Single Static Focus: A clearly defined marker that acts as a trust signal.

This is a visual verification tool, not a navigation app. If your users need to plan a route or explore the neighborhood, link them to Google Maps. If they just need to know "is this the right store?", use this map.

The Architectural Choice: Why Bypass React-Leaflet?

For complex GIS applications requiring dynamic layer toggling and state-driven popups, react-leaflet is excellent. However, for a "read-only" confirmation map, it introduces unnecessary overhead.

We are intentionally bypassing react-leaflet to avoid:

  • Abstraction Overhead: The wrapper library wraps Leaflet instances in React Context providers, which can complicate debugging when you just need access to the raw L.Map instance.
  • Re-render Cascades: Improperly memoized props in the wrapper can force the entire map to re-initialize or jitter, which ruins the "static" feel we want.

Direct DOM manipulation via useRef is leaner, more performant, and sufficiently stable for this specific use case.

Dependencies

pnpm add leaflet
pnpm add -D @types/leaflet

Step 1: The Resilient Client Component

We need a component that handles the browser-only Leaflet instance safely within the Next.js SSR environment.

A Note on Accessibility: Maps are notoriously difficult to make fully accessible. The implementation below adds aria-label and role attributes to signal intent to screen readers, but this does not provide full keyboard navigability within the map tiles. For full WCAG compliance, you must provide the address in plain text alongside the map.

// File: src/components/orders/LocationMap.tsx

'use client';

import { useEffect, useRef } from 'react';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';

interface LocationMapProps {
  latitude: number;
  longitude: number;
  locationName: string;
}

export function LocationMap({ latitude, longitude, locationName }: LocationMapProps) {
  const mapContainer = useRef<HTMLDivElement>(null);
  const mapInstance = useRef<L.Map | null>(null);

  useEffect(() => {
    if (!mapContainer.current) return;

    // 1. Initialize Map
    // We check if the instance exists to prevent double-initialization in React 18 strict mode.
    if (!mapInstance.current) {
      mapInstance.current = L.map(mapContainer.current, {
        zoomControl: true,       // Keep zoom for basic usability
        scrollWheelZoom: false,  // CRITICAL: Prevents users from getting "stuck" while scrolling the page
        dragging: !L.Browser.mobile, // Optional: improved stability on touch devices
      }).setView([latitude, longitude], 15);

      // 2. The "Shopify" Aesthetic: CartoDB Positron Tiles
      // These tiles are free for non-commercial use, but check licensing for high-volume stores.
      L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
        attribution: '&copy; OpenStreetMap &copy; CARTO',
        subdomains: 'abcd',
        maxZoom: 20,
      }).addTo(mapInstance.current);
      
      // 3. Custom Marker
      // Inline styles are used here for simplicity, but in production, 
      // classNames are preferred for better CSP compliance and theming.
      const customIcon = L.divIcon({
        className: 'custom-map-marker', 
        html: `<div style="background-color: #000; width: 100%; height: 100%; border-radius: 50%; border: 2px solid #fff; box-shadow: 0 2px 4px rgba(0,0,0,0.3);"></div>`,
        iconSize: [16, 16], // Small, subtle marker
        iconAnchor: [8, 8],
      });

      L.marker([latitude, longitude], { icon: customIcon })
        .addTo(mapInstance.current);
    }

    // 4. Defensive Cleanup
    // While checkout pages are often terminal, we clean up the instance 
    // to prevent WebGL context leaks during client-side navigation.
    return () => {
      if (mapInstance.current) {
        mapInstance.current.remove();
        mapInstance.current = null;
      }
    };
  }, [latitude, longitude]);

  return (
    <div 
      ref={mapContainer} 
      className="h-64 w-full rounded-lg border border-gray-200 z-0 grayscale-[20%]"
      // These attributes provide context, but do not replace text-based alternatives
      role="img" 
      aria-label={`Map showing pickup location: ${locationName}`}
    />
  );
}

Step 2: The Server Integration

We will wrap our map in a Suspense boundary. This allows the critical "Order Confirmed" text and receipt details to render immediately, while the heavier map logic loads in the background.

Production Reality Check: The example below abstracts away data fetching details. In a real application, fetchOrder would need to handle:

  • Authentication: Verifying the session cookie.
  • Caching: Determining if the order data is fresh or stale.
  • Error Handling: What happens if the order service is down?

// File: src/app/order/[id]/page.tsx

import { Suspense } from 'react';
import { notFound } from 'next/navigation';
import { LocationMap } from '@/components/orders/LocationMap';

// 1. Skeleton for perceived performance
// This prevents layout shift while the Leaflet CSS/JS bundles load.
function MapSkeleton() {
  return <div className="h-64 w-full bg-gray-50 animate-pulse rounded-lg border border-gray-100" />;
}

async function OrderMapContainer({ orderId }: { orderId: string }) {
  // Abstracted data fetch - implement your own Auth/Cache logic here
  const order = await fetchOrder(orderId); 

  if (!order) return null;

  return (
    <LocationMap
      latitude={order.location.lat}
      longitude={order.location.lng}
      locationName={order.location.name}
    />
  );
}

export default function OrderPage({ params }: { params: { id: string } }) {
  return (
    <div className="max-w-2xl mx-auto py-12 px-4">
      <div className="text-center mb-8">
        <h1 className="text-3xl font-bold">Order #1024</h1>
        <p className="text-gray-600">Thank you for your purchase.</p>
      </div>
      
      <div className="border rounded-xl p-6 bg-white shadow-sm">
        <h2 className="font-semibold mb-4">Pickup Location</h2>
        
        {/* 2. Isolated Streaming */}
        <Suspense fallback={<MapSkeleton />}>
          <OrderMapContainer orderId={params.id} />
        </Suspense>
        
        <div className="mt-4 text-center">
          <a href="#" className="text-sm text-blue-600 hover:underline">
            Get Directions (Google Maps)
          </a>
        </div>
      </div>
    </div>
  );
}

Conclusion: Trust Signal vs. Utility

The implementation above achieves the "Shopify" look by prioritizing constraints over features. We removed scroll zooming, color, and complex popups to create a component that says "We are professional" rather than "Here is a map tool."

When to skip the map entirely: If your goal is purely performance or if you have zero budget for tile usage limits, consider generating a Static Map Image using the Mapbox or Google Static Maps API. A static image has zero hydration cost, is easier to make accessible (it's just an <img> tag), and often fulfills the exact same "trust signal" requirement as an interactive map.

However, if you need that slight touch of interactivity—allowing a user to verify the cross-street or neighborhood context—this minimal Leaflet implementation offers the best balance of aesthetics and engineering cost.

Thanks, Matija

0

Frequently Asked Questions

Comments

Leave a Comment

Your email will not be published

10-2000 characters

• Comments are automatically approved and will appear immediately

• Your name and email will be saved for future comments

• Be respectful and constructive in your feedback

• No spam, self-promotion, or off-topic content

Matija Žiberna
Matija Žiberna
Full-stack developer, co-founder

I'm Matija Žiberna, a self-taught full-stack developer and co-founder passionate about building products, writing clean code, and figuring out how to turn ideas into businesses. I write about web development with Next.js, lessons from entrepreneurship, and the journey of learning by doing. My goal is to provide value through code—whether it's through tools, content, or real-world software.