Skip to content

Vite ​

Any Vite app (vanilla, React, Vue, Svelte, Solid...) can use every format. This page uses vanilla TypeScript.

Full example

sh
npm install pennants

Stop Vite from inlining the flags ​

Each stylesheet (pennants/css, pennants/css/rect, pennants/css/square, pennants/css/circle) references 271 flag SVGs. By default, Vite inlines every asset under 4 KiB as a data URI, which puts most flags into your CSS. Emit them as files instead:

ts
import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    assetsInlineLimit: (file) => (file.includes('/pennants/svg/') ? false : undefined),
  },
});

Or import the CDN variant (pennants/css/cdn, pennants/css/circle/cdn...). Its image URLs point to jsDelivr, so Vite has nothing to inline or emit.

import 'pennants/css'CSS (min / gzip / brotli)Flag images
Vite defaults376.2 kB / 53.0 kB / 43.8 kBmost inlined in the CSS
With assetsInlineLimit above15.7 kB / 3.8 kB / 2.8 kB271 files, fetched when used
pennants/css/cdn instead26.3 kB / 2.0 kB / 1.2 kBfrom jsDelivr, fetched when used

Usage ​

ts
// Stylesheet for the `pn pn-xx` classes (pennant shape).
import 'pennants/css';
// Flag artwork: only the flags you import are bundled. 1x1 art draws
// pennant, square and circle; 4x3 art draws rect.
import ar from 'pennants/art/1x1/ar';
import ar4x3 from 'pennants/art/4x3/ar';
import { getPennantName, loadFlag, renderFlag, toPennantCode } from 'pennants';
// Static files as hashed URLs.
import deUrl from 'pennants/svg/pennant/de.svg?url';
import spriteUrl from 'pennants/sprite?url';

// Same art, any shape (synchronous, works during SSR too).
document.querySelector('#circle')!.innerHTML = renderFlag(ar, { shape: 'circle' });
document.querySelector('#rect')!.innerHTML = renderFlag(ar4x3, { shape: 'rect' });

// Runtime codes: loads only this flag's chunk.
const code = toPennantCode('BR'); // 'br'; throws RangeError on unknown codes
const lazy = document.querySelector('#lazy')!;
lazy.innerHTML = await loadFlag(code, { shape: 'square' });
lazy.setAttribute('role', 'img');
lazy.setAttribute('aria-label', getPennantName(code));

document.querySelector<HTMLImageElement>('#img')!.src = deUrl;
document.querySelector('#sprite')!.setAttribute('href', `${spriteUrl}#pn-mx`);

Each shape has its own stylesheet and base class, so they mix on one page. Add import 'pennants/css/circle', then:

html
<span class="pn pn-ar pn-2x" role="img" aria-label="Argentina"></span>
<span class="pn-circle pn-ar pn-2x" role="img" aria-label="Argentina"></span>
<svg width="33" height="36" role="img" aria-label="Mexico"><use id="sprite" /></svg>

Only insert markup from renderFlag, loadFlag or fetchPennant with innerHTML. Never insert user input this way.

pennants/sprite contains pennants only. For the ?url imports, add "types": ["vite/client"] to your tsconfig.

Bundle impact ​

Measured with Vite 8, production build, compared with an empty app:

You useEntry JSLazy JS files ¹
pennants/art/1x1/ar + renderFlag (pennant)+4.6 kB / +1.7 kB / +1.6 kB0
pennants/art/1x1/ar + renderFlag (circle)+4.6 kB / +1.7 kB / +1.6 kB0
Both ar art sets + renderFlag (all four shapes)+7.0 kB / +2.0 kB / +1.8 kB0
getPennantName+1.8 kB / +1.0 kB / +785 B0
pennantUrl+1.7 kB / +899 B / +661 B0
loadPennant+5.1 kB / +2.5 kB / +2.1 kB544
loadFlag, circle + rect+5.1 kB / +2.5 kB / +2.1 kB544
fetchPennant (pennants/fetch)+4.1 kB / +2.1 kB / +1.7 kB0
pennants/element/register+11.9 kB / +5.2 kB / +4.4 kB545

Vite 8.3.5 · min / gzip / brotli · 2026-10-22

Art modules are the lightest way to show a few known flags, and one art module serves every shape. loadFlag and the Web Component cost a fixed few KB, then one small chunk per flag on demand. fetchPennant from pennants/fetch skips bundled chunks entirely and fetches svg/<shape>/<code>.svg from the CDN (or your base).

MIT licensed. Flag artwork from flag-icons (MIT).