DEV Community

Cover image for Using GeoIcons in Vanilla JavaScript and Static HTML (Zero Build Step)
Rushan
Rushan

Posted on Originally published at geoicons.io

Using GeoIcons in Vanilla JavaScript and Static HTML (Zero Build Step)

Icon packages often target specific JavaScript frameworks or bundlers. Static websites, server-rendered templates in Django, Laravel, Rails, or plain HTML prototypes need an approach that works with standard DOM APIs. GeoIcons provides a vanilla JavaScript package (@geoicons/vanilla) that creates native SVG elements in the browser without bundlers, compilers, or build configurations. Adding an icon takes three steps: import the icon factory from a CDN, call the factory with your desired dimensions, and append the result to the DOM.

<script type="module">
  import { UnitedStates, France, Japan } from 'https://esm.sh/@geoicons/vanilla/countries';

  const container = document.getElementById('icons');
  container.append(
    UnitedStates({ size: 40, 'aria-label': 'United States' }),
    France({ size: 40, 'aria-label': 'France' }),
    Japan({ size: 40, 'aria-label': 'Japan' })
  );
</script>
Enter fullscreen mode Exit fullscreen mode

See it live on geoicons.io →

That is the entire path. Below: zero-build CDN imports, DOM factory options, declarative HTML scanning, dynamic updates with htmx, and CSS styling.

Key takeaways

  • Load @geoicons/vanilla through an ES module CDN in any <script type="module"> tag.
  • Call icon factories like UnitedStates() or France() to generate live SVGSVGElement nodes.
  • Use createGeoIcons() to scan HTML for data-geoicon attributes and replace placeholder elements with SVG markup.
  • Supply an explicit icon map to createGeoIcons() so the browser downloads the icons you display.

Zero build setup with native ES modules

Modern browsers parse ES modules inside <script type="module"> tags without bundlers or package managers. CDNs such as esm.sh serve npm packages as standards-compliant ES modules with support for package subpath exports.

Create an index.html file and load the icons from the CDN:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>GeoIcons Static Demo</title>
  <style>
    .icon-list {
      display: flex;
      gap: 1rem;
      align-items: center;
    }
    .geoicon {
      color: #0f172a;
    }
  </style>
</head>
<body>
  <h1>Countries</h1>
  <div id="icon-list" class="icon-list"></div>

  <script type="module">
    import { UnitedStates, France, Japan } from 'https://esm.sh/@geoicons/vanilla/countries';

    const container = document.getElementById('icon-list');

    const us = UnitedStates({ size: 36, class: 'geoicon', 'aria-label': 'United States' });
    const fr = France({ size: 36, class: 'geoicon', 'aria-label': 'France' });
    const jp = Japan({ size: 36, class: 'geoicon', 'aria-label': 'Japan' });

    container.append(us, fr, jp);
  </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Open this file in any browser or serve it from a local development server like Python's http.server. The browser downloads the three requested icon modules.

Choosing a CDN provider

You can load @geoicons/vanilla from any CDN provider that distributes npm packages, including esm.sh, jsDelivr, unpkg, or cdnjs:

<!-- esm.sh -->
<script type="module">
  import { UnitedStates } from 'https://esm.sh/@geoicons/vanilla/countries';
</script>

<!-- jsDelivr -->
<script type="module">
  import { UnitedStates } from 'https://cdn.jsdelivr.net/npm/@geoicons/vanilla/dist/countries/base/index.js/+esm';
</script>

<!-- unpkg -->
<script type="module">
  import { UnitedStates } from 'https://unpkg.com/@geoicons/vanilla/dist/countries/base/index.js?module';
</script>
Enter fullscreen mode Exit fullscreen mode

Providers like esm.sh resolve subpaths declared in the package exports field (/countries, /areas, or /subdivisions). Providers like jsDelivr and unpkg accept the path to the compiled entry file inside the dist directory.

Icon factory options and attributes

Each icon export is a function that creates and returns a live SVGSVGElement. The factory accepts an options object with two dedicated properties and forwards every other property as an SVG attribute:

  • size: sets both width and height attributes. Defaults to 24.
  • strokeWidth: sets the stroke-width attribute. Defaults to 1.
  • Additional keys: sets the corresponding attribute on the <svg> node, including class, style, stroke, fill, and data-*.
import { Germany } from 'https://esm.sh/@geoicons/vanilla/countries';

const icon = Germany({
  size: 48,
  strokeWidth: 1.5,
  class: 'country-flag text-blue-600',
  style: 'display: inline-block; vertical-align: middle;',
  'aria-label': 'Germany',
  'data-code': 'DE'
});

document.body.append(icon);
Enter fullscreen mode Exit fullscreen mode

The function returns a standard browser SVGSVGElement. You can attach event listeners, inspect attributes, or insert the element with standard DOM methods like append(), prepend(), or replaceWith().

Declarative HTML with createGeoIcons

Calling DOM append methods for every icon becomes tedious in multi-page sites and server-rendered templates. GeoIcons exports a DOM scanner called createGeoIcons that replaces placeholder tags with their corresponding SVG markup.

Place empty tags in your HTML with a data-geoicon attribute that names the icon:

<div class="country-row">
  <i data-geoicon="us" class="geoicon" aria-label="United States"></i>
  <i data-geoicon="fr" class="geoicon" aria-label="France"></i>
  <i data-geoicon="jp" class="geoicon" aria-label="Japan"></i>
</div>
Enter fullscreen mode Exit fullscreen mode

Import createGeoIcons and pass an object that maps attribute values to icon factories:

<script type="module">
  import { createGeoIcons } from 'https://esm.sh/@geoicons/vanilla';
  import { Us, France, Japan } from 'https://esm.sh/@geoicons/vanilla/countries';

  createGeoIcons({
    us: Us,
    fr: France,
    jp: Japan,
  });
</script>
Enter fullscreen mode Exit fullscreen mode

The scanner finds every matching [data-geoicon] element, builds the SVG with the mapped factory, copies class, style, and aria-label attributes to the new element, and calls replaceWith().

Countries export both their ISO 3166-1 alpha-2 code (Us, Fr, Jp) and their full country name in PascalCase (UnitedStates, France, Japan). You can use whichever naming style fits your project.

Tree-shaking with explicit icon maps

GeoIcons ships 799 geographic icons. Most applications need a subset of the catalog. Passing an explicit { key: factory } map to createGeoIcons lets your bundler and the browser download the icons you request:

import { createGeoIcons } from 'https://esm.sh/@geoicons/vanilla';
import { UnitedStates, Japan } from 'https://esm.sh/@geoicons/vanilla/countries';

// Only UnitedStates and Japan enter the network transfer
createGeoIcons({
  us: UnitedStates,
  jp: Japan,
});
Enter fullscreen mode Exit fullscreen mode

Because createGeoIcons holds no internal icon registry, unimported icons remain out of your bundle. Placeholder tags whose keys do not exist in the supplied map remain untouched in the DOM.

Scanning dynamic DOM insertions (htmx and fetch)

Single-page navigation and libraries like htmx or Hotwire Turbo swap HTML fragments into the document after the initial page load. Call createGeoIcons again after inserting markup. Pass a root option to scan the injected container rather than the entire document:

import { createGeoIcons } from 'https://esm.sh/@geoicons/vanilla';
import { UnitedStates, Germany } from 'https://esm.sh/@geoicons/vanilla/countries';

const iconMap = {
  us: UnitedStates,
  de: Germany,
};

// Scan initial document
createGeoIcons(iconMap);

// Scan content injected by htmx
document.body.addEventListener('htmx:afterSwap', (event) => {
  createGeoIcons(iconMap, { root: event.detail.target });
});
Enter fullscreen mode Exit fullscreen mode

Passing the swap target prevents rescanning elements that already hold rendered SVGs.

Styling dimensions and colors with CSS

Every icon renders with fill="none" and stroke="currentColor". The SVG inherits the current text color of its parent container. You control color and dimensions through utility classes or stylesheet rules:

.geoicon {
  width: 2rem;
  height: 2rem;
  color: #0284c7;
  stroke-width: 1.25;
  transition: color 200ms ease, transform 200ms ease;
}

.geoicon:hover {
  color: #0369a1;
  transform: scale(1.05);
}
Enter fullscreen mode Exit fullscreen mode

Because CSS takes precedence over SVG presentation attributes, utility classes like Tailwind's w-8 h-8 text-sky-600 apply to the rendered SVG.

Accessibility: informative vs decorative icons

Screen readers need clear markup to interpret vector graphics. GeoIcons manages accessible attributes based on the presence of an aria-label option:

Providing aria-label instructs the factory to assign role="img", generate a <title> tag with a unique ID, and set aria-labelledby. Omitting aria-label causes the factory to set aria-hidden="true", hiding the graphic from assistive devices.

<!-- Standalone icon: needs aria-label -->
<i data-geoicon="us" class="geoicon" aria-label="United States"></i>

<!-- Paired with visible text: decorative -->
<span class="country-pill">
  <i data-geoicon="us" class="geoicon"></i>
  <span>United States</span>
</span>
Enter fullscreen mode Exit fullscreen mode

Omit aria-label if text beside the icon names the country. The icon remains decorative, and screen readers announce the country name once.

Areas and subdivisions

GeoIcons organizes icons into three categories with separate export subpaths:

  • @geoicons/vanilla/countries: 255 sovereign states and territories under their ISO 3166-1 alpha-2 codes (Us, Fr) and full names (UnitedStates, France).
  • @geoicons/vanilla/areas: 167 supranational regions, continents, and landforms under PascalCase slugs (EuropeanUnion, NorthAmerica, Asean).
  • @geoicons/vanilla/subdivisions: 377 regional states, provinces, and districts under their parent alpha-2 code plus region name (UsCalifornia, UsTexas, CaOntario).

Import areas and subdivisions from their subpaths:

<script type="module">
  import { createGeoIcons } from 'https://esm.sh/@geoicons/vanilla';
  import { EuropeanUnion } from 'https://esm.sh/@geoicons/vanilla/areas';
  import { UsTexas } from 'https://esm.sh/@geoicons/vanilla/subdivisions';

  createGeoIcons({
    eu: EuropeanUnion,
    texas: UsTexas,
  });
</script>

<i data-geoicon="eu" class="geoicon" aria-label="European Union"></i>
<i data-geoicon="texas" class="geoicon" aria-label="Texas"></i>
Enter fullscreen mode Exit fullscreen mode

Bundler workflow with npm

Projects with a build step can install @geoicons/vanilla from npm:

npm i @geoicons/vanilla
Enter fullscreen mode Exit fullscreen mode

Import specifiers match the CDN paths. Bundlers read the package exports field and drop unimported icons during production builds:

import { createGeoIcons } from '@geoicons/vanilla';
import { UnitedStates, Japan } from '@geoicons/vanilla/countries';

createGeoIcons({
  us: UnitedStates,
  jp: Japan,
});
Enter fullscreen mode Exit fullscreen mode

The package declares "sideEffects": false in its package.json, enabling tree-shaking across Vite, Rollup, webpack, and Parcel.

FAQ

Can I use GeoIcons in HTML without Node.js or a bundler?

Yes. Load @geoicons/vanilla from an ES module CDN such as esm.sh, jsDelivr, or unpkg inside a standard <script type=\

Why does createGeoIcons require an explicit object map?

An explicit map prevents loading the entire icon library. By requiring you to pass the icons you need, bundlers and CDNs deliver those specific files rather than all 799 icons.

How do I change the color of a vanilla GeoIcon?

Set the CSS color property on the icon element or its parent container. GeoIcons use stroke=\

Does createGeoIcons work with dynamic DOM updates like htmx?

Yes. Listen for your framework DOM update event and run createGeoIcons with the root option pointing to the swapped container element.

Where to go next

Read the full component documentation in the API reference, or explore the icon catalog to find icon names. For projects using component frameworks, GeoIcons also ships dedicated packages for React, Vue, and Angular.

Top comments (0)