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-brighthas 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
getPaintPropertyonce. Many defaults are zoom expressions, not plain colors. - Markers: one Marker Icon API parameter set per theme.
iconTypeis independent of the pin shape;noWhiteCirclewith a whitecontentColordraws the icon right on the theme color, andshadowType=glowsuits a dark Halloween map. - Static maps too: the Static Maps API
styleCustomizationparameter takes the same layer ids and colors, so newsletter and email images can match the interactive map.
On this page
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 technique has three parts, and the rest of the post covers them in order:
- Palette: one color per group of style layers, applied with
map.setPaintProperty(). - Date rules: a small table of date ranges that returns a theme id for today.
- 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:
| Group | osm-bright layer ids | Paint property | Christmas |
|---|---|---|---|
background | background | background-color | #fbfcff |
landuse | landuse-residential | fill-color | #f1f4f8 |
landuseOther | landuse-commercial, landuse-industrial, landuse-school, landuse-hospital, landuse-railway, landuse-cemetery | fill-color | #eef1f6 |
park | landcover-grass, landcover-grass-park (the park layer is hidden, see below) | fill-color | #2f7d4a |
wood | landcover-wood | fill-color | #1e5a35 |
water | water, water-offset; waterway-river, waterway-stream-canal, waterway-other, waterway_tunnel | fill-color; line-color | #bfe3f5 |
building | building | fill-color, fill-outline-color | #e3e9f0 |
buildingTop | building-top | fill-color, fill-outline-color | #f4f7fa |
roadMajor | highway-primary, highway-trunk, highway-motorway, highway-motorway-link, and their bridge- and tunnel- layers | line-color | #e8a0aa |
roadMajorCasing | the -casing layers of the main roads | line-color | #c47883 |
roadMid | highway-secondary-tertiary, highway-link, and their bridge- and tunnel- layers | line-color | #f5cdd2 |
roadMidCasing | the -casing layers of those roads | line-color | #d9a5ac |
roadMinor | highway-minor, tunnel-minor, tunnel-service-track; highway-area | line-color; fill-color | #ffffff |
roadMinorCasing | highway-minor-casing, tunnel-minor-casing, tunnel-service-track-casing | line-color | #d8dee8 |
path | highway-path, bridge-path, tunnel-path | line-color | #d4a017 |
rail | railway, railway-transit, railway-service, bridge-railway, tunnel-railway | line-color | #9aa6b5 |
label | place-*, highway-name-*, waterway-name, water-name-*, poi-level-1/2/3, poi-railway | text-color | #1e5a35 |
labelHalo | the same label layers | text-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
pathuntouched would keep the gold paths. - Save values, not assumptions. Many
osm-brightdefaults are zoom expressions. Thebuildingfill, 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({})restoresosm-bright. - Hide the
parklayer in a theme. Despite its name, inosm-brightit 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 inlandcover-grassandlandcover-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.
| Theme | Date window | Rule |
|---|---|---|
| Christmas | Dec 1 – Jan 6 | Range that crosses the new year |
| Valentine's Day | Feb 7 – 14 | Range |
| St. Patrick's Day | Mar 14 – 17 | Range |
| Easter | Palm Sunday – Easter Monday | Calculated each year |
| Halloween | Oct 24 – 31 | Range, checked before Autumn |
| Winter | Jan 7 – end of February | Range |
| Summer | Jun 21 – Aug 31 | Range |
| Autumn | Sep 22 – Nov 30 | Range |
| Default | All other days | No 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.
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:
typevsiconType.typeis the background shape, andiconTypeis the icon library used to resolveicon. They are independent, so a Font Awesome gift box can sit on a Material pin. WheniconTypeis omitted, the library follows the shape.noWhiteCirclewithcontentColor=#ffffff. By default the icon sits on a white circle, so a white icon would be invisible.noWhiteCircleremoves the circle, and the white icon is drawn right on the red or green pin.contentSizefor wide icons. The Font Awesomegiftsicon is wider than it is tall; on a 46 px pin it fits atcontentSize=18.type=plain. If you use plain markers (an icon with no background shape), the icon color comes fromcolor, notcontentColor.
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.
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_skiingandsledding. - Valentine's Day (Feb 7 – 14): pink land and deep pink roads; Font Awesome pins (
type=awesome) withheart,gemandchampagne-glasses. - St. Patrick's Day (Mar 14 – 17): green roads and gold secondary roads; Tabler pins with
clover,clover-2andrainbow. - Summer (Jun 21 – Aug 31): warm sand land, turquoise water and coral roads; Material Symbols Rounded pins with
sunny,beach_accessandicecream. - Autumn (Sep 22 – Nov 30): ochre parks, rust woods and brick roads; Lucide pins with
leaf,wheatandapple.
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.
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
¢er=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_KEYWhat 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
;, andnoneto hide a layer. For a text layer the color sets the label color. - URL-encode colors.
#must be sent as%23; in JavaScript,URLSearchParamsorencodeURIComponent()does it for you. - Markers use the Marker Icon API options. Static Maps
markertakestype,icontype,icon,size,contentsize,contentcolorandwhitecircle:no(the equivalent ofnoWhiteCircle), and several markers are separated by|. Seticontypeexplicitly 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:
- Get a free API key: sign in to Geoapify MyProjects, create a project and copy its API key.
- Create an empty folder for prompts 1 and 2, or open your project folder for prompt 3.
- In that folder, create a plain text file named
.envwith 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. - 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 withYOUR_API_KEYin the savedstyle.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.pngshowed 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.htmlloads 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 inosm-bright(fill-opacity0.2 forpark, 0.1 forlandcover-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_typestreet, confidence 1), and each marker anchor landed within 0.5 px of its square. - A trap worth knowing: in
osm-bright, theparklayer 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 inlandcover-grassandlandcover-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-gl6.16.0 (pinned), aseasonal-map.jsmodule, a config file and a.gitignore. Five existing files changed by 17 lines, and the build passed. - It copied the key from
.envinto a git-ignored.env.localasVITE_GEOAPIFY_API_KEYand 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-gl6 has no default export, soimport 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 tomaplibregl.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 devto 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.
- CodePen: Seasonal map style by date
- GitHub: maplibre-seasonal-map-style-by-date in the Geoapify quickstart examples. Open
src/index.htmlin a browser; no build step is needed.
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.
What's Next
These resources go deeper into map styles and markers:
- Styling a map: customize colors and labels: change colors in Maputnik or in code with
setPaintProperty. - Customize a map style: add a cycleways layer: how vector styles work, and how to add new layers, not only recolor existing ones.
- How to change and customize a map style in the Geoapify docs.
- Map Tiles: all Geoapify map styles, including light, grey and dark bases.
- Map Marker Icon API: shapes, icon libraries, outlines and shadows.
- Static Maps API: map images with markers, geometries and style customization.
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.
