Seasonal Map Styles in MapLibre GL: Switch Your Map to Christmas, Easter, Halloween by Date

Recolor Geoapify vector tiles by date with MapLibre GL, switch to seasonal markers, and reuse the palette on static maps

Christmas-styled map of Strasbourg with tree, gift and star markers, Easter and Halloween maps behind, and season chips
Christmas-styled map of Strasbourg with tree, gift and star markers, Easter and Halloween maps behind, and season chips

A seasonal map style is the same vector tiles with different colors: recolor the style layers with MapLibre setPaintProperty when the map loads, pick the palette by date, and swap the markers for seasonal icons. There is no second style file to host and no tile set to switch on the server. When the season ends, the date rule stops matching and the default style comes back without a deploy. This post walks through a tested example on the Geoapify osm-bright style in central Strasbourg, from the layer ids to the date rules, the markers, and a matching static map image.

Key takeaways

  • Same tiles: osm-bright has 111 layers (20 fill, 63 line, 27 symbol, 1 background). A palette of 18 colors on layer groups is enough for a full theme.
  • By date: month-day ranges plus a calculated Easter date pick the theme; holidays are checked before the longer seasons, so they win.
  • Restore: save the original values with getPaintProperty once. Many defaults are zoom expressions, not plain colors.
  • Markers: one Marker Icon API parameter set per theme. iconType is independent of the pin shape; noWhiteCircle with a white contentColor draws the icon right on the theme color, and shadowType=glow suits a dark Halloween map.
  • Static maps too: the Static Maps API styleCustomization parameter takes the same layer ids and colors, so newsletter and email images can match the interactive map.

What Is a Seasonal Map Style?

A seasonal map style is a temporary color theme applied on top of an existing vector map style. The vector tiles carry geometry and attributes, while the style decides how each layer is painted. Because MapLibre GL renders the tiles in the browser, you can change those paint properties at runtime and the map redraws immediately.

For a store locator, a delivery area map or a city guide, that means a Christmas look in December without touching the data, the map setup or the server. The comparison below shows the same Strasbourg view in the default osm-bright style and in the Christmas palette from this post.

The same map of central Strasbourg twice: on the left the default osm-bright style with beige land, yellow main roads and light green parks; on the right the Christmas style with dark green parks, rose main roads, gold footpaths, green labels and red and green Christmas tree, gift and candy cane markers
Left: default osm-bright. Right: the same tiles with the Christmas palette and seasonal markers. Icons: Font Awesome Free (CC BY 4.0), Material Icons pin shape (Apache 2.0). Markers generated with the Geoapify Marker Icon API. Powered by Geoapify, © OpenMapTiles, map data © OpenStreetMap contributors.

The technique has three parts, and the rest of the post covers them in order:

  1. Palette: one color per group of style layers, applied with map.setPaintProperty().
  2. Date rules: a small table of date ranges that returns a theme id for today.
  3. Markers: a Marker Icon API parameter set per theme, so the icons change with the colors.

Which Style Layers Make a Map Look Like Christmas?

The osm-bright style is served at https://maps.geoapify.com/v1/styles/osm-bright/style.json?apiKey=YOUR_API_KEY, and in our check on November 4, 2026 it had 111 layers: 63 line, 27 symbol, 20 fill and 1 background. You don't need a color for each of them. Layers that draw the same kind of feature (all main roads, all casings, all labels) share a group, and a theme sets one color per group.

These are the 18 groups used in the example, with the Christmas color for each:

Grouposm-bright layer idsPaint propertyChristmas
backgroundbackgroundbackground-color#fbfcff
landuselanduse-residentialfill-color#f1f4f8
landuseOtherlanduse-commercial, landuse-industrial, landuse-school, landuse-hospital, landuse-railway, landuse-cemeteryfill-color#eef1f6
parklandcover-grass, landcover-grass-park (the park layer is hidden, see below)fill-color#2f7d4a
woodlandcover-woodfill-color#1e5a35
waterwater, water-offset; waterway-river, waterway-stream-canal, waterway-other, waterway_tunnelfill-color; line-color#bfe3f5
buildingbuildingfill-color, fill-outline-color#e3e9f0
buildingTopbuilding-topfill-color, fill-outline-color#f4f7fa
roadMajorhighway-primary, highway-trunk, highway-motorway, highway-motorway-link, and their bridge- and tunnel- layersline-color#e8a0aa
roadMajorCasingthe -casing layers of the main roadsline-color#c47883
roadMidhighway-secondary-tertiary, highway-link, and their bridge- and tunnel- layersline-color#f5cdd2
roadMidCasingthe -casing layers of those roadsline-color#d9a5ac
roadMinorhighway-minor, tunnel-minor, tunnel-service-track; highway-arealine-color; fill-color#ffffff
roadMinorCasinghighway-minor-casing, tunnel-minor-casing, tunnel-service-track-casingline-color#d8dee8
pathhighway-path, bridge-path, tunnel-pathline-color#d4a017
railrailway, railway-transit, railway-service, bridge-railway, tunnel-railwayline-color#9aa6b5
labelplace-*, highway-name-*, waterway-name, water-name-*, poi-level-1/2/3, poi-railwaytext-color#1e5a35
labelHalothe same label layerstext-halo-color#ffffff

The Christmas look comes from a few strong groups. Dark green parks and woods, rose main roads with a darker rose casing, and gold footpaths do most of the work. Land, water and buildings stay pale, so markers and labels remain readable. Rose (#e8a0aa) instead of a saturated red keeps the main roads festive without competing with the red markers.

Find a Layer Id in the Maps Playground

You can read the layer ids from style.json, but clicking is faster. In the Geoapify Maps playground, click a feature on the map: the layer editor lists the layers under the cursor, lets you pick a new color or hide a layer, and generates the matching map.setPaintProperty(...) code for MapLibre GL.

The same layer editor is built into the Static Maps playground, where your edits become styleCustomization entries for a static image instead. More on that in the static maps section.

How Do You Build the Christmas Style in MapLibre GL?

The Christmas style is two objects and one function. layerGroups maps each group to [layer id, paint property] pairs, the palette maps groups to colors, and applyPalette() writes the colors. The code below is simplified from the quickstart example, which uses MapLibre GL 5.6.0 and all 18 groups.

const map = new maplibregl.Map({
  container: "my-map",
  style: "https://maps.geoapify.com/v1/styles/osm-bright/style.json?apiKey=YOUR_API_KEY",
  center: [7.7470, 48.5823], // central Strasbourg
  zoom: 15.4
});

const layerGroups = {
  background: [["background", "background-color"]],
  park: ["landcover-grass", "landcover-grass-park"].map((id) => [id, "fill-color"]),
  water: [["water", "fill-color"], ["waterway-river", "line-color"]],
  roadMajor: ["highway-primary", "highway-trunk", "highway-motorway"].map((id) => [id, "line-color"]),
  path: ["highway-path", "bridge-path", "tunnel-path"].map((id) => [id, "line-color"]),
  label: ["place-city", "highway-name-minor", "highway-name-major"].map((id) => [id, "text-color"])
};

const christmasPalette = {
  background: "#fbfcff",
  park: "#2f7d4a",
  water: "#bfe3f5",
  roadMajor: "#e8a0aa",
  path: "#d4a017",
  label: "#1e5a35"
};

// Save the original paint values once, after the style has loaded
const originalPaint = [];

map.on("load", () => {
  Object.values(layerGroups).flat().forEach(([layerId, property]) => {
    if (map.getLayer(layerId)) {
      originalPaint.push([layerId, property, map.getPaintProperty(layerId, property)]);
    }
  });

  applyPalette(christmasPalette);
});

function applyPalette(palette) {
  // Restore the default style first, so themes never mix
  originalPaint.forEach(([layerId, property, value]) => map.setPaintProperty(layerId, property, value));

  Object.entries(palette).forEach(([group, color]) => {
    layerGroups[group].forEach(([layerId, property]) => {
      if (map.getLayer(layerId)) {
        map.setPaintProperty(layerId, property, color);
      }
    });
  });

  // The "park" layer holds protected areas: hide it while a theme is on
  if (map.getLayer("park")) {
    map.setLayoutProperty("park", "visibility", Object.keys(palette).length ? "none" : "visible");
  }
}

A few details make this robust:

  • Restore before applying. Without the restore step, switching from Christmas to a theme that leaves path untouched would keep the gold paths.
  • Save values, not assumptions. Many osm-bright defaults are zoom expressions. The building fill, for example, is {"base": 1, "stops": [[15.5, "#f2eae2"], [16, "#dfdbd7"]]}. getPaintProperty() returns that expression, so restoring it brings back the exact default, zoom behavior included.
  • Check the layer exists. map.getLayer() guards against ids that a different base style doesn't have, so the same code works if you switch the base style.
  • An empty palette is the default style. applyPalette({}) restores osm-bright.
  • Hide the park layer in a theme. Despite its name, in osm-bright it also holds protected areas: historic districts such as Strasbourg's Grande Île, and nature reserves. A park color on it tints whole old towns. City parks and gardens are in landcover-grass and landcover-grass-park, so they stay green.

setPaintProperty only changes how MapLibre draws the tiles it already has. No tiles are downloaded again, and the map redraws right away.

How Do You Switch the Style by Date?

A date rule is either a month-day range or a calculated holiday. The example keeps them in one ordered array and returns the first match. Holidays come first, so Valentine's Day wins over Winter and Halloween wins over Autumn.

ThemeDate windowRule
ChristmasDec 1 – Jan 6Range that crosses the new year
Valentine's DayFeb 7 – 14Range
St. Patrick's DayMar 14 – 17Range
EasterPalm Sunday – Easter MondayCalculated each year
HalloweenOct 24 – 31Range, checked before Autumn
WinterJan 7 – end of FebruaryRange
SummerJun 21 – Aug 31Range
AutumnSep 22 – Nov 30Range
DefaultAll other daysNo rule matched

The code compares dates as numbers: December 15 becomes 1215. When from is greater than to, the range crosses the new year.

const seasonRules = [
  { theme: "christmas", from: [12, 1], to: [1, 6] },
  { theme: "valentine", from: [2, 7], to: [2, 14] },
  { theme: "stpatrick", from: [3, 14], to: [3, 17] },
  { theme: "easter", easter: true },
  { theme: "halloween", from: [10, 24], to: [10, 31] },
  { theme: "winter", from: [1, 7], to: [2, 29] },
  { theme: "summer", from: [6, 21], to: [8, 31] },
  { theme: "autumn", from: [9, 22], to: [11, 30] }
];

function matchesRule(rule, date) {
  if (rule.easter) {
    const easter = getEasterSunday(date.getFullYear());
    const from = new Date(easter.getFullYear(), easter.getMonth(), easter.getDate() - 7); // Palm Sunday
    const to = new Date(easter.getFullYear(), easter.getMonth(), easter.getDate() + 1);   // Easter Monday
    return date >= from && date <= to;
  }

  const value = (date.getMonth() + 1) * 100 + date.getDate();
  const from = rule.from[0] * 100 + rule.from[1];
  const to = rule.to[0] * 100 + rule.to[1];

  return from <= to ? value >= from && value <= to : value >= from || value <= to;
}

function getThemeForDate(date) {
  const rule = seasonRules.find((item) => matchesRule(item, date));
  return rule ? rule.theme : "default";
}

// Compare calendar days: today at midnight, local time
const now = new Date();
const todayAtMidnight = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const themeId = getThemeForDate(todayAtMidnight);

getEasterSunday() is a short function in the full example that implements the Anonymous Gregorian algorithm (also known as the Meeus/Jones/Butcher algorithm). It returns April 5 for 2026 and March 28 for 2027, so the Easter window runs March 29 – April 6 in 2026 and March 21 – 29 in 2027. Passing a date at midnight matters for this rule: Easter Monday at 10:00 would be later than the to date at 00:00.

Local date or a fixed time zone? new Date() uses the visitor's time zone, so a visitor in Tokyo sees Christmas hours before one in New York. If everyone should switch at the same moment, for example at midnight in Strasbourg, read the calendar day in that zone: new Intl.DateTimeFormat("en-CA", { timeZone: "Europe/Paris" }).format(new Date()) returns a yyyy-mm-dd string you can split into year, month and day.

Force a Theme for a Campaign or a Preview

The calendar is not always the right trigger. A campaign may start in mid-November, or you may want to review the Christmas look before December. The example accepts an override from the page URL or from a constant in the code:

const forcedTheme = null; // for example "christmas"

const requestedTheme = new URLSearchParams(window.location.search).get("theme") || forcedTheme;

// themes: the palettes and marker settings, keyed by theme id
const activeTheme = requestedTheme && themes[requestedTheme]
  ? requestedTheme
  : getThemeForDate(todayAtMidnight);

?theme=halloween in the page URL wins over forcedTheme, and with neither the theme follows the date. Only known theme ids are accepted (default, christmas, winter, valentine, stpatrick, easter, summer, autumn, halloween), so a typo in a shared link falls back to the automatic choice instead of breaking the map.

How Do You Switch the Markers with the Season?

Each theme in the example has one set of Marker Icon API parameters (shape, size, shadow) plus one icon per place. The eight seasons use seven different marker types between them, from Material and Font Awesome pins to Tabler, Phosphor, Lucide and Material Symbols Rounded pins and circles. The grid below draws seasonal icons on every marker type.

A grid of seasonal map markers: one row per theme (Christmas, Winter, Valentine's Day, St. Patrick's Day, Easter, Summer, Autumn, Halloween) and one column per marker shape, with icons such as a Christmas tree, a snowflake, a heart, a clover, an egg, an ice cream, a leaf and a ghost in each theme's colors
Seasonal icons on every Marker Icon API marker type. Icons and pin shapes: Font Awesome Free (CC BY 4.0; the awesome pin is based on Font Awesome location-dot), Material Icons and Material Symbols (Apache 2.0), Tabler Icons (MIT), Phosphor Icons (MIT), Lucide (ISC), Material Design Icons (Pictogrammers Free License). Markers generated with the Geoapify Marker Icon API.

The Christmas markers are Material pins with Font Awesome icons. The helper builds one URL per place:

const christmas = {
  marker: { type: "material", iconType: "awesome", size: "46", contentSize: "18" },
  icons: ["tree", "gifts", "star", "candy-cane", "snowflake", "holly-berry"],
  colors: ["#c8102e", "#1e5a35"]
};

function getMarkerIconUrl(theme, index) {
  const params = new URLSearchParams(theme.marker);
  params.set("icon", theme.icons[index]);
  params.set("color", theme.colors[index % theme.colors.length]);
  params.set("contentColor", "#ffffff");
  params.set("noWhiteCircle", "true");
  params.set("scaleFactor", "2");
  params.set("apiKey", "YOUR_API_KEY");

  return `https://api.geoapify.com/v2/icon?${params.toString()}`;
}

The parameters that matter for seasonal sets:

  • type vs iconType. type is the background shape, and iconType is the icon library used to resolve icon. They are independent, so a Font Awesome gift box can sit on a Material pin. When iconType is omitted, the library follows the shape.
  • noWhiteCircle with contentColor=#ffffff. By default the icon sits on a white circle, so a white icon would be invisible. noWhiteCircle removes the circle, and the white icon is drawn right on the red or green pin.
  • contentSize for wide icons. The Font Awesome gifts icon is wider than it is tall; on a 46 px pin it fits at contentSize=18.
  • type=plain. If you use plain markers (an icon with no background shape), the icon color comes from color, not contentColor.

Place the markers with the anchor headers. Every marker response carries Icon-Width, Icon-Height, Icon-Anchor-X and Icon-Anchor-Y, and a HEAD request returns them without the image. All markers of a theme share one layout, because only the icon and color differ, so the example sends one HEAD request per theme and sets each MapLibre marker's offset to [-anchorX, -anchorY]. The Marker Icon API update post explains why shadows move the anchor, and the MapLibre marker anchors example has the complete code.

Attribution: the icons and pin shapes come from open-source libraries: Font Awesome Free (CC BY 4.0, which also covers the outline of type=awesome pins), Material Icons and Material Symbols (Apache 2.0), Tabler Icons, Phosphor Icons, Lucide and Material Design Icons. Font Awesome requires attribution; see the Marker Icon API attribution rules for each library.

What Do Easter, Halloween and Other Seasons Look Like?

The same 18 groups produce very different maps. Easter and Halloween are the two extremes, a pastel light map and a dark one, and the dark one needs one extra step.

Two maps of central Strasbourg side by side: an Easter style with pale cream land, lilac buildings, mint parks, yellow secondary roads, pink footpaths and pastel Phosphor pins with egg, tulip and butterfly icons; and a Halloween style with a dark purple background, orange main roads, light orange labels and glowing circle markers with pumpkin, ghost and bat icons
Easter (left) and Halloween (right) on the same osm-bright tiles. Icons and pin shapes: Phosphor Icons (MIT), Material Design Icons (Pictogrammers Free License). Markers generated with the Geoapify Marker Icon API. Powered by Geoapify, © OpenMapTiles, map data © OpenStreetMap contributors.

Easter: a Moving Date and a Pastel Palette

The Easter palette uses cream land (#fffdf3), mint parks (#b7e4c7), purple main roads (#9c6ade), yellow secondary roads (#ffd54f) and pink paths (#f48fb1). The markers are Phosphor pins (type=phosphor, so the Phosphor library is used without setting iconType) with the icons egg, flower-tulip, butterfly, flower, bird and plant.

Easter is the only rule that isn't a fixed range. The theme runs from Palm Sunday to Easter Monday, and Easter Sunday is calculated for each year: April 5, 2026 and March 28, 2027.

Halloween: a Dark Map Needs Flipped Halos

The Halloween palette turns osm-bright into a dark map: a #1b1325 background, dark purple land and buildings, orange main roads (#ff7518) and light orange labels (#ffb86b). Recoloring the text alone is not enough. The default osm-bright labels have a white halo (rgba(255,255,255,0.8)), which would draw bright outlines around every street name on a dark map, so the labelHalo group sets text-halo-color to the background color, #1b1325.

The markers are circles with Material Design Icons (type=circle&iconType=mdi): halloween, ghost, skull, bat, spider and candle. shadowType=glow with shadowColor=#ff7518 gives them an orange halo that stands out on the dark tiles. If your product needs a dark theme for a longer period, Geoapify also has ready-made dark styles such as dark-matter in the map styles list.

Winter, Valentine's Day, St. Patrick's Day, Summer and Autumn

The full example includes five more themes, each with its own marker type:

  • Winter (Jan 7 – end of February): icy blue land and navy roads; Material Icons circles with a drop shadow, such as ac_unit, downhill_skiing and sledding.
  • Valentine's Day (Feb 7 – 14): pink land and deep pink roads; Font Awesome pins (type=awesome) with heart, gem and champagne-glasses.
  • St. Patrick's Day (Mar 14 – 17): green roads and gold secondary roads; Tabler pins with clover, clover-2 and rainbow.
  • Summer (Jun 21 – Aug 31): warm sand land, turquoise water and coral roads; Material Symbols Rounded pins with sunny, beach_access and icecream.
  • Autumn (Sep 22 – Nov 30): ochre parks, rust woods and brick roads; Lucide pins with leaf, wheat and apple.

Can a Static Map Image Use the Same Palette?

Yes. The Static Maps API renders the same vector styles on the server and accepts a styleCustomization parameter in the format layer:color;size|layer:none. The layer names are the same osm-bright layer ids, so the palette converts into layer:color pairs one to one. This is useful wherever JavaScript can't run: newsletter images, social cards, and order confirmation emails in the season's look.

Static map image of central Strasbourg in the Christmas style, with dark green parks, rose main roads, gold footpaths, green labels and six red and green pins with Christmas tree, gifts, star, candy cane, snowflake and holly icons
One Static Maps API request with the Christmas palette (77 styleCustomization entries) and six markers. Icons: Font Awesome Free (CC BY 4.0), Material Icons pin shape (Apache 2.0). Powered by Geoapify, © OpenMapTiles, map data © OpenStreetMap contributors.

Here is the structure of the request, shortened to five layers and one marker. Line breaks are added for readability:

https://maps.geoapify.com/v1/staticmap?style=osm-bright&width=1000&height=600
  &center=lonlat:7.7470,48.5823&zoom=15.2&scaleFactor=2
  &styleCustomization=park:%232f7d4a|water:%23bfe3f5|highway-primary:%23e8a0aa|highway-path:%23d4a017|place-city:%231e5a35
  &marker=lonlat:7.750156,48.581555;type:material;color:%23c8102e;icontype:awesome;icon:tree;size:46;contentsize:18;contentcolor:%23ffffff;whitecircle:no
  &apiKey=YOUR_API_KEY

What to know when you convert a theme:

  • One color per layer. The docs list color changes for fill, line and text layers, an optional width or text size after ;, and none to hide a layer. For a text layer the color sets the label color.
  • URL-encode colors. # must be sent as %23; in JavaScript, URLSearchParams or encodeURIComponent() does it for you.
  • Markers use the Marker Icon API options. Static Maps marker takes type, icontype, icon, size, contentsize, contentcolor and whitecircle:no (the equivalent of noWhiteCircle), and several markers are separated by |. Set icontype explicitly so the static markers use the same library as the interactive ones.

Use POST for a full palette. With all 77 Christmas layer entries and six markers, our encoded GET URL was about 3,750 characters. For long customizations, the Static Maps API accepts an HTTP POST request with the parameters in a JSON body, where styleCustomization becomes an array of { layer, color, size } objects and markers become objects in a markers array. Colors in the JSON body don't need URL encoding:

curl -X POST "https://maps.geoapify.com/v1/staticmap?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -o strasbourg-christmas.jpg \
  -d '{
    "style": "osm-bright",
    "width": 1000,
    "height": 600,
    "center": { "lon": 7.747, "lat": 48.5823 },
    "zoom": 15.2,
    "scaleFactor": 2,
    "styleCustomization": [
      { "layer": "park", "color": "#2f7d4a" },
      { "layer": "water", "color": "#bfe3f5" },
      { "layer": "highway-primary", "color": "#e8a0aa" },
      { "layer": "highway-path", "color": "#d4a017" },
      { "layer": "place-city", "color": "#1e5a35" }
    ],
    "markers": [
      { "lon": 7.750156, "lat": 48.581555, "type": "material", "color": "#c8102e",
        "icontype": "awesome", "icon": "tree", "size": 46, "contentsize": 18,
        "contentcolor": "#ffffff", "whitecircle": "no" }
    ]
  }'

In our test, this request returned a JPEG image with the recolored parks, paths and city label and the tree marker. To build styleCustomization visually, open the Static Maps playground and click the layers you want to change; the style customization docs have more examples.

How Do You Build It with an AI Coding Agent?

You can also hand the job to a coding agent such as Claude Code, Codex or Cursor. The three prompts below start from different places: generating the theme only, a new standalone page, and a module in an existing project. We ran each one in Claude Code, in an empty folder or a small Vite project, and tightened the wording wherever the agent had to guess. The versions below include those fixes.

Before you start. You don't need to write code, but the agent needs an API key and a folder to work in:

  1. Get a free API key: sign in to Geoapify MyProjects, create a project and copy its API key.
  2. Create an empty folder for prompts 1 and 2, or open your project folder for prompt 3.
  3. In that folder, create a plain text file named .env with one line: GEOAPIFY_API_KEY= followed by your key. The prompts tell the agent to read the key from this file, so you never paste it into the chat.
  4. Open the folder in your coding agent, paste the prompt, and let it work. Each prompt ends by asking the agent to check its own result and tell you how to open it.

Whatever the agent produces, check three things: every layer id exists in the style, the key appears only where the browser needs it, and the markers are placed with the Icon-Anchor-* headers. A key used in a web page is visible to its visitors, so restrict it to your website's domain in MyProjects before you publish.

Prompt 1: Generate the Theme and the Icons

This prompt produces only the building blocks, a layer-group map, a palette and the marker URLs, which you can drop into any MapLibre GL map:

Create a Christmas theme for a MapLibre GL map that uses the Geoapify osm-bright style:
https://maps.geoapify.com/v1/styles/osm-bright/style.json?apiKey=...
My Geoapify API key is in the .env file in this folder, as GEOAPIFY_API_KEY=... Read it from
there. Don't show the key in your answers and don't copy it into any other file.

1. Download style.json and list its layers by id and type. The file contains the key in its
   source, sprite and glyphs URLs: replace it with YOUR_API_KEY before saving.
2. Group the layers that draw the same kind of feature: background, land use, parks, woods,
   water, buildings, main / secondary / minor roads and their casings, paths, railways,
   labels and label halos. Use only layer ids that exist in this style.
3. Write christmas-theme.js that exports:
   - layerGroups: for each group, [layerId, paintProperty] pairs (fill-color, line-color,
     background-color, text-color, text-halo-color);
   - palette: one color per group. Snowy pale land and buildings, dark green parks and woods,
     icy blue water, rose main roads (not red, so they don't compete with red markers),
     gold footpaths, dark green labels on white halos; pick matching colors for the other
     groups. Park and wood layers are semi-transparent: also set their fill-opacity in
     christmas-theme.js so the dark green stays dark. Set label halos on every label layer,
     even where the style has none.
   - markers(apiKey): a function that returns six Christmas marker URLs from the Geoapify Marker Icon API
     (https://apidocs.geoapify.com/docs/icon/): type=material, iconType=awesome with
     Font Awesome 6 Free icons tree, gifts, star, candy-cane, snowflake, holly-berry,
     alternating #c8102e and #1e5a35, contentColor=#ffffff, noWhiteCircle, size=46,
     contentSize=18, scaleFactor=2. Build the URLs with URLSearchParams.
4. Make preview.html that shows the osm-bright map of central Strasbourg (lon 7.747,
   lat 48.582, zoom 15) with the palette applied (setPaintProperty after the style loads)
   and the six markers. preview.html takes the key from ?apiKey= in its address, so the
   key stays out of the file. Open it in a browser yourself and save a screenshot as
   preview.png, so I can see the result without setting anything up. Then tell me in
   one or two simple steps how to open preview.html myself, with the exact command and
   the address to paste into the browser.
   Check that every marker shows its icon and that labels are readable, then show me
   the palette as a table.

What happened in our test:

  • The agent read the key from .env, replaced it with YOUR_API_KEY in the saved style.json, and kept it out of every file it produced.
  • It sorted the 111 layers into 16 groups. A check against the style found no missing layer ids and no paint property on the wrong layer type.
  • preview.png showed snowy land, icy water, dark green parks, rose main roads, gold paths, six red and green markers with white icons, and readable labels.
  • Two gaps we closed in the prompt. preview.html loads the theme as an ES module, which browsers don't run from a double-clicked file, so the agent now gives the exact command and address to open it. Parks and woods are semi-transparent in osm-bright (fill-opacity 0.2 for park, 0.1 for landcover-wood), so the theme now sets their opacity too.

Prompt 2: A Standalone Seasonal Map

This prompt asks for a complete page with three themes, the date rules, geocoded places and anchored markers:

Create a standalone index.html (no build step) that opens by double-click, with a MapLibre
GL JS map on the Geoapify osm-bright vector style, centered on central Strasbourg. Load
MapLibre from a CDN with a classic <script> tag: use the newest release that still ships
dist/maplibre-gl.js (newer ones are ES-module only and can't start their worker from a
file:// page), pinned to an exact version. The map switches to a seasonal theme by date:
- Christmas: Dec 1 – Jan 6. Easter: Palm Sunday to Easter Monday (calculate Easter Sunday
  for the year). Halloween: Oct 24 – 31. Any other day: the default style.
  Compare calendar days in the visitor's local date, ignoring the time of day.
- ?theme=christmas|easter|halloween|default in the page URL forces a theme.

Style:
- Recolor layers with map.setPaintProperty after the style loads. Before applying a theme,
  save the original values with map.getPaintProperty and restore them, so themes never mix.
- Recolor the background, land use (landuse-*), parks and woods (landcover-grass,
  landcover-grass-park, landcover-wood; keep the park layer, which holds protected areas,
  faint), water, buildings, highway-area, all roads with their casings, paths and all label
  layers; anything left out shows as a bright patch on a dark map. On dark themes also fade
  the water-pattern layer and the highway-shield icons (icon-opacity). Christmas: snowy land, dark green parks, rose main roads,
  gold paths. Easter: pastel. Halloween: dark, with orange roads and labels, and
  text-halo-color of the label layers set to the dark background.
- Use only layer ids that exist in https://maps.geoapify.com/v1/styles/osm-bright/style.json.

Markers:
- One marker at each of six squares: Place de la Cathédrale, Place Broglie, Place Kléber,
  Place Gutenberg, Place Benjamin Zix, Place du Marché aux Poissons. Get their coordinates
  once with the Geoapify Geocoding API (https://apidocs.geoapify.com/docs/geocoding/) and
  hardcode them in the page.
- Marker images from the Geoapify Marker Icon API (https://apidocs.geoapify.com/docs/icon/),
  one parameter set per theme (shape, size, shadow; only icon and color change per marker),
  contentColor=#ffffff with noWhiteCircle for white icons, size=46 for pins and 34 for
  circles, contentSize=20, scaleFactor=2, displayed at the Icon-Width x Icon-Height size:
  Christmas: type=material, iconType=awesome, red #c8102e and green #1e5a35 pins, contentSize=18,
  tree, gifts, star, candy-cane, snowflake, holly-berry;
  Easter: type=phosphor, purple #9c6ade and yellow #f2b705 pins, egg, flower-tulip, butterfly,
  flower, bird, plant;
  Halloween: type=circle, iconType=mdi, orange #ff7518 and purple #6a1b9a circles,
  halloween, ghost, skull, bat, spider, candle, with shadowType=glow;
  default: type=material blue #1e88e5 pins with a star icon.
- Send one HEAD request per theme and place markers with the Icon-Anchor-X and
  Icon-Anchor-Y response headers.

The style already carries the attribution "Powered by Geoapify | © OpenMapTiles ©
OpenStreetMap contributors": keep MapLibre's attribution control expanded and don't add a
second copy.
My Geoapify API key is in the .env file in this folder, as GEOAPIFY_API_KEY=... Put it into
one GEOAPIFY_API_KEY constant at the top of the page, and remind me to restrict the key to my
website in Geoapify MyProjects before I publish the page.
Then open the page in a browser yourself for ?theme=christmas, ?theme=easter and
?theme=halloween, save a screenshot of each, and tell me which theme today's date picks and
how I open the page myself.

What happened in our test:

  • Without a version in the prompt, the agent chose MapLibre GL JS 5.24.0: the newest release that still ships dist/maplibre-gl.js, because 6.x is ES-module only. Loaded with a classic <script> tag, the page ran from a double-clicked file with no errors. An earlier run with the current 6.x release failed on a double-clicked file with "Worker failed to load".
  • All 11 edge dates we checked picked the right theme, including Palm Sunday and Easter Monday in 2026 and 2027 at any time of day. After Christmas, switching back to the default restored all 128 saved paint values exactly.
  • The Geocoding API found all six squares (result_type street, confidence 1), and each marker anchor landed within 0.5 px of its square.
  • A trap worth knowing: in osm-bright, the park layer also holds protected areas. In Strasbourg it returned one feature for the centre, {"class": "patrimoine mondial", "name": "Grande Île"}, the UNESCO area, so "dark green parks" turned the whole old town green in an earlier run. City parks are in landcover-grass and landcover-wood, and the prompt now says so.
  • The agent's answer told the reader which theme today picks, how to open the page and add ?theme=christmas, and to restrict the key in MyProjects before publishing. On the dark map, white highway shield icons stayed bright, so the prompt now fades them too.

Prompt 3: Add a Seasonal Map Module to an Existing Project

This prompt is for a project that already exists. The agent has to read the project first and fit the map into it:

Add a map of our event locations to this project as a reusable module, with a seasonal
look that follows the date.

Before you start
- Inspect the project first. Reuse its framework, bundler, package manager, folder
  structure and code style, and make the smallest change that works.
- If the project already has a MapLibre GL map, extend it. Otherwise add maplibre-gl with
  an exact version and use the Geoapify osm-bright vector style:
  https://maps.geoapify.com/v1/styles/osm-bright/style.json?apiKey=...
  maplibre-gl 6 has no default export (import * as maplibregl from "maplibre-gl") and loads
  its worker from a separate file: when you bundle it, set the worker URL with
  maplibregl.setWorkerUrl as the MapLibre docs describe.
- My Geoapify API key is in the .env file in the project folder, as GEOAPIFY_API_KEY=...
  Read it the way the project reads other settings. If the build tool needs another name or
  file (Vite: VITE_GEOAPIFY_API_KEY in .env.local), set that up, make sure the file is
  git-ignored, and tell me what you changed. Never hardcode, print or log the key.

The module
- createSeasonalMap(container, places, options) creates the map with one marker per place,
  and applySeasonalTheme(map, date, forcedTheme) switches themes: Christmas (Dec 1 – Jan 6),
  Halloween (Oct 24 – 31), default otherwise. ?theme=christmas|halloween|default in the
  page URL forces a theme for testing.
- Christmas: snowy land, dark green parks, rose main roads, gold paths. Halloween: a dark
  map with orange roads and light labels; recolor land use, highway-area and paths too, and
  set text-halo-color of the labels to the dark background.
- Keep the palettes and marker settings in a separate config file. Recolor with
  setPaintProperty; save the defaults with getPaintProperty once and restore them before
  each theme.
- Markers come from the Geoapify Marker Icon API (https://apidocs.geoapify.com/docs/icon/),
  placed with the Icon-Anchor-X / Icon-Anchor-Y headers of one HEAD request per theme,
  with white icons (contentColor=#ffffff and noWhiteCircle) and scaleFactor=2.
  Christmas: type=material, iconType=awesome, icon=tree on red #c8102e pins.
  Halloween: type=circle, iconType=mdi, icon=ghost on orange #ff7518 circles, shadowType=glow.
  Default: type=material, icon=event on blue #1e88e5 pins.
- Keep the map attribution visible, and add Font Awesome attribution to the map's
  attribution control (customAttribution) while a theme uses type=awesome or iconType=awesome.

Finish
- Show the map where the events are listed, build the project, and report the files you
  changed and the command to start the site. Mention that a browser app ships the key in
  its bundle, so the key should be restricted to our domains in Geoapify MyProjects.

What happened in our test:

  • In a small Vite 5 site that listed Strasbourg events, the agent added maplibre-gl 6.16.0 (pinned), a seasonal-map.js module, a config file and a .gitignore. Five existing files changed by 17 lines, and the build passed.
  • It copied the key from .env into a git-ignored .env.local as VITE_GEOAPIFY_API_KEY and explained that step in plain words. The key is in no source file. Vite inlines it into the built bundle, as with any browser app, so restrict it to your domains in Geoapify MyProjects.
  • maplibre-gl 6 has no default export, so import maplibregl from "maplibre-gl" breaks the build; import * as maplibregl from "maplibre-gl" works. It also loads its worker from a separate file, which a bundler doesn't pick up by itself: without a hint, an earlier run failed with "Worker failed to load" until the agent passed the worker file to maplibregl.setWorkerUrl(). The prompt now mentions both.
  • Markers landed within 0.5 px of the event coordinates, the date edges were right, and switching themes restored the exact defaults. The Font Awesome credit appeared only for the Christmas theme, after a first try on an empty source that never showed; the prompt now points to the attribution control.
  • The final answer listed the changed files and npm run dev to start the site.

Try the Full Example

The complete example puts everything together: six squares in central Strasbourg (Place de la Cathédrale, Place Broglie, Place Kléber, Place Gutenberg, Place Benjamin Zix and Place du Marché aux Poissons), eight seasonal themes, the date rules, a date picker to preview any day, and a theme selector. A status box shows which theme is active and whether it was picked by date or forced.

The example includes a demo key for a quick preview. For your own project, get a free API key in Geoapify MyProjects and replace the key in src/script.js.

Gallery of eight maps of central Strasbourg, one per seasonal theme: Christmas, Winter, Valentine's Day, St. Patrick's Day, Easter, Summer, Autumn and Halloween, each with its own palette and marker set
Eight seasons on the same Geoapify osm-bright vector tiles. Icons and pin shapes: Font Awesome Free (CC BY 4.0), Material Icons and Material Symbols (Apache 2.0), Tabler Icons (MIT), Phosphor Icons (MIT), Lucide (ISC), Material Design Icons (Pictogrammers Free License). Markers generated with the Geoapify Marker Icon API. Powered by Geoapify, © OpenMapTiles, map data © OpenStreetMap contributors.

What's Next

These resources go deeper into map styles and markers:

Frequently Asked Questions

Do I need a separate style.json for a Christmas map style?

No. Load the regular Geoapify style, for example osm-bright, and recolor its layers in the browser with MapLibre setPaintProperty after the map loads. One palette object per theme is enough. See the seasonal map style example and the setPaintProperty reference.

Does recoloring the map download the tiles again or cost extra requests?

No tiles are downloaded again: setPaintProperty changes how MapLibre GL draws the vector tiles it already has, so recoloring adds no map tile requests. New seasonal markers are new images: each Marker Icon API request costs 1 credit, so generate them once per theme and reuse them. See Map Tiles pricing, Marker Icon API pricing and Geoapify pricing.

How do I switch the map back to the default style after the holidays?

Save the original paint values with getPaintProperty once after the style loads, and restore them before applying any palette. When no date rule matches, apply an empty palette and the default style comes back without a deploy. Saving the values matters because many defaults are zoom expressions. See the getPaintProperty reference.

Can I use the seasonal colors in a static map image?

Yes. The Static Maps API styleCustomization parameter takes layer:color pairs with the same layer ids, for example park:%232f7d4a|highway-path:%23d4a017. For a full palette, send a POST request with styleCustomization as an array of layer and color objects. See Map Style Customization and HTTP POST requests.

Which icon libraries have Christmas, Easter and Halloween icons, and do I need attribution?

In our example, Font Awesome Free has tree, gifts, candy-cane and holly-berry, Phosphor Icons has egg, flower-tulip and butterfly, and Material Design Icons has halloween, ghost and bat. Font Awesome icons and the type=awesome pin require Font Awesome attribution (CC BY 4.0); Material Icons and Material Symbols (Apache 2.0) need none. On the Free plan, add Geoapify attribution. See Marker Icon API attribution.

Does this work with Leaflet?

Not with raster tiles: their colors are baked into the PNG images, so they can't be recolored in the browser. If you show Geoapify vector tiles in Leaflet with the maplibre-gl-leaflet plugin, getMaplibreMap() returns the underlying MapLibre map, and you can call setPaintProperty on it. See the Leaflet vector tiles example.