Animating a Static Map with One SVG Overlay, No Map Library
Glowing office dots and flowing lines on a static map with one SVG overlay: viewBox 0-100, preserveAspectRatio none, reduced-motion-safe CSS animation.
TL;DR
Instead of Mapbox or hardcoded pixels, the author stacked an SVG with viewBox="0 0 100 100" over a static image, so office positions in percentage coordinates stretch perfectly with the viewport. Flowing dashed lines come from animating stroke-dashoffset, layered with pulsing dots, all pure CSS. The overlay stays accessible via pointer-events-none and aria-hidden, wrapped in prefers-reduced-motion.
The designer handed me a static JPEG of a Java map and asked for glowing office dots plus flowing dashed lines connecting the headquarters to the branches. On a company website, that reads like a logistics dashboard from a much bigger budget.
My first instinct was to hardcode pixel coordinates over the 1699x624 image. That approach died the moment I resized the browser window and watched the dots drift into the ocean. The second idea was pulling in Mapbox or Leaflet. Overkill: we needed a decorative layer over a static image, not geocoding or routing that bloats the JavaScript bundle.
The commit landed on a much simpler structure: one <svg> stacked directly over the next/image inside a relative container, pinned by two attributes, viewBox="0 0 100 100" and preserveAspectRatio="none" [2].
With that viewBox, one user-space unit equals one percent of the container dimension [1]. Office positions live in a single constant as estimated percentage coordinates: headquarters at x 13.2 y 34, Bandung at x 17.5 y 60, Surabaya at x 69 y 33. When the image scales with the viewport, the overlay stretches non-uniformly exactly like the raster below it. Zero JavaScript, zero recalculation on resize.
The pattern extends naturally: the same percentage trick drives the office cards' layout logic elsewhere in the codebase, because the coordinate table doubles as the single source of truth for the map region. One table, one mental model.
Layered strokes and dots, all CSS
stroke-dasharray is pure geometry, so the flow illusion comes from animating stroke-dashoffset instead of the dashes themselves [7]. Each connection is drawn twice inside one <g>: a glow layer at stroke-width 0.4 with 0.25 opacity that breathes slowly, and a dashed core at stroke-width 0.15 with strokeDasharray="0.6 0.4". That small pattern also keeps the animation loop short: shifting the offset by 2 units is enough for a seamless flow.
Office dots get four layers each: an outer pulse ring, a mid glow, a solid core, and a tiny white highlight. Radii are in viewBox units, 1.2 for HQ and 0.8 for branches, so proportions survive every screen size [1]. Colors come from var(--color-accent) to follow the theme.
<svg
viewBox="0 0 100 100"
preserveAspectRatio="none"
className="pointer-events-none absolute inset-0 h-full w-full"
aria-hidden="true"
>
{lines.map((l) => (
<g key={l.key}>
<line ... className="office-line-glow" />
<line ... strokeDasharray="0.6 0.4" className="office-line-dash" />
</g>
))}
</svg>
Every keyframe sits inside prefers-reduced-motion: no-preference [3]. Browsers re-evaluate media queries when the environment changes [8], so flipping the OS setting mid-session stops the animation without a reload. One honest gap: with prefers-contrast active, a subtle accent glow can get hard to see [6]. I chose to accept that rather than bolt on heavy outlines that would ruin the static map's look.
Invisible to pointer and screen reader
The overlay must never intercept interaction: pointer-events-none makes every click pass through to elements underneath [4], and aria-hidden="true" keeps it out of the accessibility tree since it conveys no new information [5].
@media (prefers-reduced-motion: no-preference) {
.office-dot-pulse {
animation: office-dot-pulse 2.4s ease-in-out infinite;
}
.office-line-dash {
animation: office-line-flow 3s linear infinite;
}
}
Layer order matters: pulse rings render first so they sit underneath, solid cores last so they stay sharp. And do not drop preserveAspectRatio="none". Without it the SVG switches to meet scaling, the overlay stops hugging the image edges, and percentage coordinates drift off their targets.
The debatable call: these percentage coordinates are hand-estimated from geography, not a map projection. For a decorative homepage map that is accurate enough. The moment you need real accuracy, swap in a proper mapping library instead of growing math inside the overlay.
A detail worth knowing: the position table is duplicated per locale, Pusat and Head Office carry identical coordinates because keys follow the content text. I accepted that trade-off; a normalization layer for two languages is overkill. If a third language ever arrives, move the keys to a neutral content field so geography stops riding along with translations.