You've got a delivery tracking app with 500 drop-off points across a city. Or a property search with hundreds of listings in a single postcode. You add them all as pins to your .NET MAUI Map, and the result is an unreadable mess of overlapping markers that tanks scroll performance and makes the app feel broken. Until now, your options were to reach for a third-party package like MPowerKit.GoogleMaps or Onion.Maui.GoogleMaps.Clustering, roll your own clustering algorithm with platform-specific handler code, or simply limit how many pins you show.

.NET 11 Preview 3 changes this. The built-in Map control now supports pin clustering out of the box on iOS, Mac Catalyst, and Android. One property flips it on, one property controls grouping, and one event handles taps. No external dependencies, no handler customisation, no platform-specific code.

The API at a glance

The clustering API is deliberately minimal. Three additions cover the entire surface:

On the Map control:

On the Pin class:

That's it. There's no ClusterRadius property, no MinimumClusterSize, no custom renderer pipeline. The team intentionally kept the API surface small for the initial release, leaving room for expansion in future previews.

Enabling clustering

The simplest case is a single line in XAML:

MainPage.xaml
<ContentPage xmlns:maps="http://schemas.microsoft.com/dotnet/2021/maui/maps">
    <maps:Map x:Name="map"
              IsClusteringEnabled="True"
              Region="51.5074,-0.1278,0.1,0.1" />
</ContentPage>

Or in C#:

MainPage.xaml.cs
using Map = Microsoft.Maui.Controls.Maps.Map;

var map = new Map
{
    IsClusteringEnabled = true
};

With clustering enabled, nearby pins are automatically grouped into a single cluster marker that shows a count badge. As the user zooms in, clusters expand to reveal individual pins. Zoom out, and they collapse back into clusters.

// NOTE

The Region property and its MapSpanTypeConverter are also new in .NET 11 Preview 3, replacing the verbose x:Arguments markup needed in earlier versions.

Grouping pins with ClusteringIdentifier

The real power comes when you need different pin types to cluster independently. Consider an app that shows both restaurants and petrol stations on the same map. Without identifiers, a cluster marker labelled "12" tells the user nothing about what's inside. With ClusteringIdentifier, restaurants cluster separately from petrol stations, even when they're geographically close:

Services/MapPinService.cs
public void AddLocationPins(Map map, IEnumerable<PointOfInterest> locations)
{
    foreach (var location in locations)
    {
        map.Pins.Add(new Pin
        {
            Label = location.Name,
            Address = location.Address,
            Location = new Location(location.Latitude, location.Longitude),
            ClusteringIdentifier = location.Category,
            ImageSource = ImageSource.FromFile($"{location.Category}_pin.png")
        });
    }
}

Pins with the same ClusteringIdentifier value group into the same cluster. Pins with different identifiers form separate clusters. If you don't set the property at all, every pin shares the default identifier "maui_default_cluster" and they all cluster together.

// TIP

Combine ClusteringIdentifier with the new Pin.ImageSource property (also added in Preview 3) to give each category its own marker icon. This makes individual pins visually distinct even before the user taps them.

Handling cluster taps

When a user taps a cluster marker, the default behaviour is to zoom in to reveal the individual pins. You can intercept this with the ClusterClicked event:

MainPage.xaml.cs
map.ClusterClicked += async (sender, args) =>
{
    var pinNames = args.Pins.Select(p => p.Label);
    var summary = string.Join("\n", pinNames);

    await DisplayAlertAsync(
        $"Cluster ({args.Pins.Count} locations)",
        summary,
        "OK");

    args.Handled = true;
};

The ClusterClickedEventArgs gives you three things:

Property Type Description
Pins IReadOnlyList<Pin> The pins contained in the tapped cluster
Location Location The geographic centre of the cluster
Handled bool Set to true to suppress the default zoom behaviour

Setting Handled = true is important if you're showing your own UI in response to the tap. Without it, the map will zoom in and your alert or bottom sheet will appear simultaneously.

A complete MVVM example

Here's a more realistic scenario — a delivery dashboard binding pins from a view model with clustering and category-based grouping:

Views/DeliveryMapPage.xaml
<ContentPage xmlns:maps="http://schemas.microsoft.com/dotnet/2021/maui/maps"
             xmlns:vm="clr-namespace:CourierApp.ViewModels"
             x:DataType="vm:DeliveryMapViewModel">

    <maps:Map IsClusteringEnabled="True"
              ItemsSource="{Binding Deliveries}"
              ClusterClicked="OnClusterClicked"
              Region="51.5074,-0.1278,0.15,0.15">
        <maps:Map.ItemTemplate>
            <DataTemplate x:DataType="vm:DeliveryPin">
                <maps:Pin Label="{Binding RecipientName}"
                          Address="{Binding Address}"
                          Location="{Binding Location}"
                          ClusteringIdentifier="{Binding Status}" />
            </DataTemplate>
        </maps:Map.ItemTemplate>
    </maps:Map>
</ContentPage>
ViewModels/DeliveryMapViewModel.cs
public partial class DeliveryMapViewModel : ObservableObject
{
    [ObservableProperty]
    private ObservableCollection<DeliveryPin> deliveries = [];

    public async Task LoadDeliveriesAsync()
    {
        var items = await deliveryService.GetTodaysDeliveriesAsync();

        Deliveries = new ObservableCollection<DeliveryPin>(
            items.Select(d => new DeliveryPin
            {
                RecipientName = d.RecipientName,
                Address = d.FullAddress,
                Location = new Location(d.Latitude, d.Longitude),
                Status = d.IsDelivered ? "delivered" : "pending"
            }));
    }
}

In this example, delivered and pending items cluster separately because they have different ClusteringIdentifier values ("delivered" vs "pending"). A driver can glance at the map and immediately see how many pending deliveries remain in an area.

How it works under the hood

The clustering implementation differs between platforms, which is worth understanding if you're debugging visual differences.

iOS and Mac Catalyst

On Apple platforms, MAUI delegates entirely to MapKit's native MKClusterAnnotation support (available since iOS 11). MapKit handles the spatial grouping, animation, and rendering. The ClusteringIdentifier maps directly to the native annotation view's clusteringIdentifier property.

This means clustering on iOS inherits MapKit's smooth expand/collapse animations and its battle-tested spatial algorithm. The identifier lifecycle is managed carefully — when clustering is disabled, the identifier is cleared from the view, but the model preserves the value so re-enabling clustering restores the groupings.

Android

Android takes a different approach because Google Maps for Android doesn't provide built-in clustering. The MAUI team implemented a custom grid-based algorithm rather than pulling in the Google Maps Android API Utilities library. The algorithm:

  1. Divides the visible map area into a grid based on the current zoom level
  2. Groups pins that fall within the same grid cell
  3. Creates programmatic cluster markers (blue circles with count badges)
  4. Recalculates clusters when the zoom level changes by more than 1.0 level

The threshold of 1.0 (increased from an initial 0.5 during review) reduces visual jitter when the user slowly zooms in and out. The trade-off is that clusters won't update on very small zoom changes, but in practice this feels natural.

// IMPORTANT

The custom Android implementation is designed for typical mobile pin counts — hundreds of pins work well. If you're dealing with thousands, you may still want a specialised library like MPowerKit.GoogleMaps that uses the native Google Maps Utils clustering algorithm.

Windows

Pin clustering is not supported on Windows. The Map control itself relies on the CommunityToolkit.Maui.Maps package on Windows (using a WebView-based Bing Maps implementation), and the clustering infrastructure hasn't been extended to that platform. Setting IsClusteringEnabled = true on Windows has no effect.

Combining with other Preview 3 Map features

Pin clustering was part of a broader wave of Map improvements in Preview 3. Several of these work well together:

Pages/ExplorerPage.xaml.cs
var map = new Map
{
    IsClusteringEnabled = true,
    MapStyle = jsonStyleString  // Custom JSON styling (Android only)
};

map.MapLongClicked += (s, args) =>
{
    map.Pins.Add(new Pin
    {
        Label = "Dropped Pin",
        Location = args.Location,
        ImageSource = ImageSource.FromFile("dropped_pin.png")
    });
};

// Zoom to fit all pins using the new MapSpan.FromLocations helper
var allLocations = map.Pins.Select(p => p.Location);
map.MoveToRegion(MapSpan.FromLocations(allLocations), animated: true);

The MapSpan.FromLocations method is particularly useful with clustering — it calculates a bounding region that encompasses all pin locations, so you can show the user a zoomed-out view where clusters are visible.

Common pitfalls

Forgetting to set Handled on cluster taps. If you subscribe to ClusterClicked to show custom UI but don't set args.Handled = true, the map will zoom in at the same time. The user sees your bottom sheet and the map flying to a new zoom level.

Expecting clustering on Windows. There's no error or exception — the property is silently ignored. If your app targets Windows, you'll need to handle dense pin rendering differently on that platform.

Using too many distinct ClusteringIdentifier values. If every pin has a unique identifier, nothing clusters. The identifier is meant for categories (5-10 distinct values), not individual items.

Assuming cluster markers are tappable immediately. The ClusterClicked event only fires on the cluster marker itself. Individual pins within a cluster are not tappable until the cluster is expanded by zooming in. There's no way to tap through a cluster to a specific pin underneath.

Mixing clustered and non-clustered pins. There's no per-pin opt-out. When IsClusteringEnabled is true, every pin participates in clustering. If you need some pins to remain independent (e.g. the user's current location), consider using a MapElement or an overlay approach instead.

Not testing at realistic pin counts. Clustering with 10 pins looks fine on any implementation. Test with your actual production data volume — the grid-based Android algorithm and MapKit's native algorithm produce visually different results at scale.

Summary