21 July 2026

SVG to interactive map

by Shaun Lawrence

This post is part of the MAUI UI July community series of blog posts and videos, hosted by Matt Goldman. Be sure to check out the other posts in this series!

This post was driven by the World Cup. I had been thinking about a simple way to bring a map to life in .NET MAUI, and the tournament gave me the perfect excuse to turn that idea into something concrete. As an England fan - sadly it didn’t come home - but it did at least give me a good reason to build something fun around the tournament.

The idea is hopefully pretty straightforward - take a list of countries, load the map, paint the countries, and then place the national flags inside each shape so the whole thing feels a bit more alive.

Interactive world map

The result is an interactive map that can be panned, zoomed, and tapped. The underlying approach is simple enough to be useful in a lot of real-world scenarios, whether you are building a travel app, a sports dashboard, or just having fun with custom drawing.

The world map SVG was downloaded from SimpleMaps, and the flag SVGs came from a separate flag pack. The interesting bit is not the source of the assets; it is the way we turn those SVG files into something we can draw in a .NET MAUI app.

In a follow up post, I plan to cover some of the World Cup-specific bits as well, such as loading tournament data, highlighting teams, and wiring up a richer view around the map itself.

The plan

The end result of this sample is to:

This is a nice example of how .NET MAUI can sit on top of a very low-level graphics stack without forcing us to write platform-specific code.

Creating the app

I started with a standard .NET MAUI app and added a couple of dependencies that make this easier:

The reason for using Svg.Skia is that it can load SVG images directly, which is ideal for both the world map and the individual flags.

Once the project is set up, the next step is to add the world map SVG and the flag SVGs to the app package so they can be opened via FileSystem.OpenAppPackageFileAsync.

Loading the world map SVG

The first thing we need to do is load the world SVG file and read its contents. In this sample, the world map is stored in the app package and loaded from a service class.

The important part is that the world map is not just a single giant shape; it is made up of many individual path elements, one for each country or sub-region. We need the path data for each country so we can render them later.

The code looks like this:

public async Task<List<Country>> GetCountriesAsync()
{
    using var stream = await FileSystem.OpenAppPackageFileAsync("world.svg");
    var countries = Parse(stream);

    return countries;
}

The parsing logic then opens the SVG as an XML document and looks for all path elements. Each path contains the geometry we need.

private static List<Country> Parse(Stream svgStream)
{
    var countries = new Dictionary<string, Country>();
    var doc = XDocument.Load(svgStream);

    if (doc.Root is null)
    {
        return [];
    }

    XNamespace ns = doc.Root.GetDefaultNamespace();
    var paths = doc.Descendants(ns + "path");

    foreach (var path in paths)
    {
        string id = path.Attribute("id")?.Value ?? string.Empty;
        string name = path.Attribute("name")?.Value ?? string.Empty;
        string className = path.Attribute("class")?.Value ?? string.Empty;
        string pathData = path.Attribute("d")?.Value ?? string.Empty;

        if (string.IsNullOrEmpty(pathData)) continue;

        string countryName = !string.IsNullOrEmpty(name) ? name : className;
        string isoCode = id ?? string.Empty;

        if (string.IsNullOrEmpty(countryName)) continue;

        if (!countries.TryGetValue(countryName, out var country))
        {
            country = new Country
            {
                Name = countryName,
                IsoCode = isoCode
            };
            countries[countryName] = country;
        }

        if (string.IsNullOrEmpty(country.IsoCode) && !string.IsNullOrEmpty(isoCode))
        {
            country.IsoCode = isoCode;
        }

        country.Paths.Add(pathData);
    }

    return countries.Values.ToList();
}

This is the piece that matters most. We are not trying to interpret the SVG in a browser-like way. Instead, we are extracting the raw path data and storing it in a simple model.

Each country ends up with a list of path strings that describe its outline, and that becomes the geometry we can draw later.

Turning SVG path data into drawing paths

Once we have the path data, we can turn it into Skia paths. This is where the rendering really starts.

The map is drawn inside SKCanvasView, which is the SkiaSharp equivalent of a drawing surface in a .NET MAUI page. The PaintSurface event is where we render everything.

For each country, we create a combined SKPath:

private SKPath GetCountryPath(Country country)
{
    var combinedPath = new SKPath();
    foreach (var pathData in country.Paths)
    {
        using var path = SKPath.ParseSvgPathData(pathData);
        combinedPath.AddPath(path);
    }
    return combinedPath;
}

That is the core idea. Each SVG path is converted into an SKPath, and then all the paths for a single country are merged into one path that can be filled, clipped, or hit-tested.

This is great because it means we can stay close to the original vector data while still drawing it with SkiaSharp.

Loading the flags

The flags are loaded separately from the country data. The country model holds an SKBitmap? Flag property, which is filled when the app loads the available flag files.

The loading logic looks like this:

string flagFileName = $"{country.IsoCode.ToLower()}.svg";
using var flagStream = await FileSystem.OpenAppPackageFileAsync(flagFileName);

using var svg = new SKSvg();
if (svg.Load(flagStream) != null)
{
    country.Flag = SKBitmap.FromImage(
        SKImage.FromPicture(svg.Picture, svg.Picture.CullRect.Size.ToSizeI()));
}

This is a very handy pattern. The country has a code, we turn that into a file name like fr.svg, and we load it from the app package. The flag is then converted into a bitmap that can be drawn onto the canvas.

If a flag is missing, the code just skips it and the country is drawn as a plain filled shape. That makes the map resilient even when a particular flag file is not available.

Painting the flag inside the country shape

Now comes the interesting part: taking the flag bitmap and drawing it only within the country outline.

The flow is:

  1. create the combined country path
  2. clip the canvas to that path
  3. measure the bounds of the path
  4. scale the image so it fits inside the country shape
  5. draw the bitmap into that area

The method looks like this:

private void DrawFlagInCountry(
    SKCanvas canvas,
    SKPath countryPath,
    SKBitmap flagBitmap,
    float opacity = 1.0f)
{
    canvas.Save();
    canvas.ClipPath(countryPath);

    var bounds = countryPath.Bounds;

    float scaleX = bounds.Width / flagBitmap.Width;
    float scaleY = bounds.Height / flagBitmap.Height;
    float scale = Math.Max(scaleX, scaleY);

    float width = flagBitmap.Width * scale;
    float height = flagBitmap.Height * scale;

    float x = bounds.MidX - (width / 2);
    float y = bounds.MidY - (height / 2);

    using var paint = new SKPaint
    {
        Color = SKColors.White.WithAlpha((byte)(255 * opacity))
    };

    using var image = SKImage.FromBitmap(flagBitmap);
    canvas.DrawImage(
        image,
        new SKRect(x, y, x + width, y + height),
        new SKSamplingOptions(SKFilterMode.Linear, SKMipmapMode.Linear),
        paint);

    canvas.Restore();
}

The important line is canvas.ClipPath(countryPath);. This means that everything drawn after that call is restricted to the outline of the country. In other words, the flag is painted only within the country shape.

The rest of the method is just about making the flag fit nicely. We compute the bounds of the country path, work out how large the flag image needs to be, and then draw it centered inside that bounding area.

Bringing it all together

In the main paint loop, the app iterates over all countries and decides whether to draw the flag or a fallback fill color.

foreach (var country in _viewModel.Countries)
{
    using var combinedPath = GetCountryPath(country);

    if (country.Flag is not null)
    {
        DrawFlagInCountry(canvas, combinedPath, country.Flag, opacity);
    }
    else
    {
        using var paint = new SKPaint
        {
            Color = SKColors.LightGray.WithAlpha((byte)(255 * opacity)),
            Style = SKPaintStyle.Fill,
            IsAntialias = true
        };
        canvas.DrawPath(combinedPath, paint);
    }
}

That is the whole rendering pipeline in a nutshell:

Why this approach works well

This pattern works because it keeps the geometry and the visuals separate. The map SVG gives us the shapes, and the flag SVGs give us the artwork. The renderer becomes a simple pipeline:

That makes it straightforward to extend. You could add hover states, selection highlights, tooltips, animation, or even make the flag appear with a transition. The core idea stays the same.

Current limitations

Ironically the England flag does not load - this is because the world map currently just covers the UK and not the home nations. Fear not - there will be a follow-up post covering how to do this.

A small but useful takeaway

What I like most about this approach is that it shows how far .NET MAUI can go before you need to reach for a more specialized rendering stack. You can take vector data from SVG files, parse it, and draw it in a cross-platform UI without needing to handcraft every shape yourself.

If you have a set of custom vector assets, this pattern is a great way to make them feel native to the app instead of like a static background image.

Source code

The sample code is available over on GitHub at https://github.com/bijington/world-cup.

As a further follow-up I have always loved the idea of making some form of digital photo album that takes thumbnails from photos around the world and fills them in the location they were captured. Watch this space :)

tags: C# - maui - svg - skia - graphics - MauiUiJuly