@damian-buho/support-ukraine
v2.0.1
Published
Browser library that adds a Ukraine-support charity banner to any website.
Maintainers
Readme
@damian-buho/support-ukraine
- Browser library that adds a Ukraine-support charity banner to any website.
- Shows randomly selected Ukrainian charity localized to the visitor's language and respects RTL scripts.
- You can choose kinds of charities.
- Inspired by hejny/Ukraine.
Screenshots



Install
npm
npm install @damian-buho/support-ukraineCDN (no build step)
<script type="module">
import { supportUkraineBlock } from 'https://cdn.jsdelivr.net/npm/@damian-buho/support-ukraine@2/+esm'
await supportUkraineBlock()
</script>For optimal Core Web Vitals, avoid the chained locale fetch by loading a pre-localized build:
<script type="module">
import { supportUkraineBlock } from 'https://cdn.jsdelivr.net/npm/@damian-buho/support-ukraine@2/dist/es.js'
await supportUkraineBlock()
</script>Usage
import { supportUkraineBlock } from '@damian-buho/support-ukraine'
// Auto-detect locale from navigator.language
await supportUkraineBlock()
// Force a specific locale
await supportUkraineBlock({ locale: 'es' })For optimal Core Web Vitals, let your server or router decide the language and load only the needed build — no second network request:
import { supportUkraineBlock } from '@damian-buho/support-ukraine/es'
await supportUkraineBlock()Available localized entry points: ar, de, en, es, fr, hi, it, ja, ko, nl, pl, pt, sv, th, uk, zh. The locale option is ignored in these builds — they are already localized.
If you still want to read navigator.language yourself and avoid the library’s chained fetch, detect the base
language tag and load the matching bundle:
<script type="module">
const base = navigator.language.split('-')[0]
const supported = [
'ar',
'de',
'en',
'es',
'fr',
'hi',
'it',
'ja',
'ko',
'nl',
'pl',
'pt',
'sv',
'th',
'uk',
'zh'
]
const locale = supported.includes(base) ? base : 'en'
const { supportUkraineBlock } = await import(
`https://cdn.jsdelivr.net/npm/@damian-buho/support-ukraine@2/dist/${locale}.js`
)
await supportUkraineBlock()
</script>With a bundler, do not interpolate the locale into the import specifier — Vite/webpack cannot statically resolve a computed package subpath, so the build either fails or pulls in every locale, which defeats the point. Use a literal specifier per branch instead; each one still gets its own chunk:
const bundles = {
es: () => import('@damian-buho/support-ukraine/es'),
fr: () => import('@damian-buho/support-ukraine/fr'),
// … one entry per supported locale
en: () => import('@damian-buho/support-ukraine/en')
}
const base = navigator.language.split('-')[0]
const { supportUkraineBlock } = await (bundles[base] ?? bundles.en)()
await supportUkraineBlock()In both cases only one network request is made — the pre-localized dist/<locale>.js — instead of index.js → locale chunk.
The banner is prepended to document.body by default. It displays a randomly selected charity with the format:
🇺🇦 Support Ukraine: Come Back Alive: Strengthening Ukraine's defense
The entire block is a clickable link to the charity's donation page.
Options
| Option | Type | Default | Description |
| ------------- | ----------------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------- |
| element | HTMLElement | document.body | Target mount element for the banner |
| mode | 'shift' \| 'overlap' \| 'replace' | 'shift' | 'shift' pushes page content down, 'overlap' floats on top, 'replace' swaps a same-class placeholder element |
| fontSize | string | '87.5%' | Banner font size; % anchors to the widget's own 16px host box, ignoring document root scaling |
| charities | Charity[] | (built-in) | Custom charity list; replaces the built-in database |
| tags | CharityTag[] | (all) | Filter charities by category: 'military', 'humanitarian', 'animals' |
| exclude | string[] | (none) | Exclude charities by id; unknown ids are ignored |
| dontRepeat | boolean | true | Avoid repeating charities; seen ids stay in session memory by default |
| persistSeen | boolean | false | Persist seen charity ids in localStorage across visits (needs visitor consent) |
| isInConsole | boolean | true | Log the selected charity to the dev console |
| showRefreshButton | boolean | false | Show a refresh button that loads the next random charity |
| autoRefreshInterval | number | 0 | Auto-refresh interval in milliseconds; 0 disables automatic refresh |
| showRefreshAnimation | boolean | false | Show a fade animation when the charity changes (respects prefers-reduced-motion) |
| locale | string | (auto-detected) | Override the auto-detected BCP 47 language tag |
Filtering by category
Show only military charities:
await supportUkraineBlock({ tags: ['military'] })Excluding charities
Hide specific charities by id (unknown ids are ignored):
await supportUkraineBlock({ exclude: ['united24'] })Localized charity URLs
Charities carry per-language donation URLs via a urls map (base language code → URL, en required).
The banner follows the visitor’s language and falls back to en:
await supportUkraineBlock({
charities: [
{
id: 'hospitallers',
name: 'Hospitallers',
tagline: '…',
urls: { en: 'https://www.hospitallers.org.uk', de: 'https://www.hospitallers.org.uk/de' },
tags: ['humanitarian']
}
]
})Replace mode (no layout shift)
Use replace mode to swap a same-class placeholder element (any tag) so the banner takes its place without shifting
page content:
<!-- Static HTML: renders nothing until JS runs. Mirror the host box (font-size 16px,
min-height 2.5em = the banner's reserved height) so swap-in causes zero shift. -->
<section
aria-label="Support Ukraine banner"
class="support-ukraine-block"
style="font-size:16px;min-height:2.5em"
></section>await supportUkraineBlock({ mode: 'replace' })The banner finds the first element with class support-ukraine-block inside the mount element and replaces it in place.
If no placeholder is found, it falls back to prepending. This eliminates cumulative layout shift (CLS) because the
placeholder already reserves the exact space the banner needs.
Disabling repeat prevention
Allow the same charity to appear on every page load:
await supportUkraineBlock({ dontRepeat: false })Persisting seen charities
By default the repeat prevention above keeps seen charity ids in session memory only — nothing is
written to the visitor’s device. Set persistSeen to remember them in localStorage across visits.
Storing data on a visitor’s device needs their consent under the GDPR/ePrivacy rules, so only enable
this after the site owner has obtained it:
await supportUkraineBlock({ persistSeen: true })Locale support
The banner is translated to the visitor's language automatically. The following locales are supported:
| Language | Code | RTL |
| ---------- | ---- | ------------------ |
| Arabic | ar | Yes |
| Chinese | zh | |
| Dutch | nl | |
| English | en | (default fallback) |
| French | fr | |
| German | de | |
| Hindi | hi | |
| Italian | it | |
| Japanese | ja | |
| Korean | ko | |
| Polish | pl | |
| Portuguese | pt | |
| Spanish | es | |
| Swedish | sv | |
| Thai | th | |
| Ukrainian | uk | |
RTL scripts are detected automatically and the banner direction is set accordingly.
For best performance, pick the locale yourself (from Accept-Language, <html lang>, or your i18n router) and import the matching entry point. The generic dist/index.js uses navigator.language and a dynamic import() for the locale chunk — that chained request delays the banner and hurts CWV. Each dist/<locale>.js bundles its messages statically, so it renders in a single fetch.
Architecture
src/
├── index.ts # Auto-detect build (navigator.language + dynamic import)
├── banner.ts # Shared banner rendering (mountBanner, DEFAULT_CHARITIES)
├── types.ts # Charity, SupportUkraineBlockOptions, etc.
├── i18n.ts # Locale detection, loading, merging (generic build only)
├── locales/ # Per-locale translation files
├── charities.yaml # Built-in charity database
├── entries/ # Per-locale entry points (ar.ts, es.ts, …) — static import, no fetch
└── styles.scss # Banner CSS (compiled by tsup)
dist/
├── index.js # Generic auto-detect build + locale chunks (en-*.js, es-*.js …)
├── en.js, es.js … # Pre-localized self-contained builds — one fetch, no chained request
└── index.d.ts # Types (shared by all entry points)Contributing
See CONTRIBUTING.md for development setup and guidelines.
