<!-- GENERATED by scripts/build-llms.mjs from llms/media.md — do not edit this file. -->

# `lr-flag`

- **Import** `import '@aceshooting/lyra-ui/components/lr-flag.js';` (stable tag alias; registers the tag)
- **Class** `LyraFlag`, also available unregistered from `@aceshooting/lyra-ui/components/media/flag/flag.class.js`
- **Family** `components/media/` — see `llms/index.md` for its siblings
- **Status** `stable` since `4.0.0` — see the maturity and deprecation policy in `llms/shared.md`
- **Release history** [CHANGELOG.md](../../CHANGELOG.md); family-wide breaking-change summaries: [llms-full.txt](../../llms-full.txt)
- **Deprecations** none
- **Optional peers** none
- **Themeable via** 3 parts, 3 custom properties — see this component's own `@csspart`/`@cssprop` list below
- **Library-wide behavior** (events, form association, `locale`/`strings`, tokens, TS types): `llms/shared.md`

---

## `lr-flag`

Country/language flag image. Flag artwork ships in a **separate, optional peer package**
(`@aceshooting/lyra-flags`) — importing `lyra-ui` core never pulls in flag image weight.

**Properties:**

- `country?: string` (ISO 3166-1 **alpha-2 or alpha-3**, e.g. `"fr"` or `"FRA"` — takes
  precedence over `language`). Length alone disambiguates the two code spaces, so no format hint
  is needed: a 2-letter value is alpha-2, a 3-letter value is alpha-3. Alpha-3 support exists
  because public statistical sources (World Bank, UN, IMF, most open-data portals) key country
  records on alpha-3; the 249 officially-assigned mappings are packed as a ~1.2 KB fixed-width
  string and expanded lazily on the first alpha-3 lookup, so an alpha-2-only app never pays for
  them. Withdrawn and user-assigned codes deliberately do not map to a successor state — they
  take the unresolved path below
- `fallback?: string` — placeholder image URL rendered when the code cannot resolve to a current
  flag. The `fallback` slot wins over it
- `language?: string` (BCP-47-ish tag, e.g. `"en"`/`"en-US"`, resolved to a representative country
  via `languageToCountry()`)
- `src?: string` (a pre-resolved flag image URL — takes precedence over `country`/`language` and
  skips the peer-package lookup; mainly useful to avoid even
  the small per-flag async hop when you already have the URL at build time, e.g. from
  `import frUrl from '@aceshooting/lyra-flags/flags/fr.svg?url'`. `label` is effectively required
  alongside `src` since there's no `country`/`language` to derive a fallback `alt` from.)
- `label?: string` (accessible name / `alt` text — **defaults to a localized, human-readable region
  name derived from the _resolved country_ code via `Intl.DisplayNames` if omitted**, see gotchas)
- host `aria-label` takes precedence over `label` and the derived region name; an explicit empty
  value marks the image decorative
- `shape: LyraFlagShape = 'rect'` (reflected, `'rect' | 'circle'`)
- `fidelity: LyraFlagFidelity = 'standard'` (reflected — picks a
  fidelity tier for the ~65 codes whose source art embeds a coat of arms/seal/emblem; every other
  code resolves to the same file regardless of `fidelity`. `'compact'` = a tiny WebP raster for
  icon-scale use (menus, language pickers, ~12–28px); `'standard'` = the icon-optimized vector for
  card/row sizes (~28–96px); `'detailed'` = the
  pristine full-fidelity vector for hero-scale display. No effect when `src` is set.)

The v9 vocabulary replaces `round` with `shape="circle"` and `variant` with `fidelity`; exported
authoring types are `LyraFlagShape`, `LyraFlagFidelity`, and `LyraFlagUrlResolver`.

**Events:** none.

**Slots:** `fallback` — rendered in place of the flag when `country`/`language` cannot resolve to a
current flag (an unassigned, historical, or malformed code). Wins over the `fallback` property.

**CSS parts:** `image` (the underlying `<img>`, exposed once native loading succeeds),
`fallback-image` (the `fallback` property's placeholder, when no slot content is supplied), `error`
(contained localized visible text rendered when URL validation, peer resolution, or native image
loading fails). The host reflects the terminal error state with `data-error`, and an unresolvable
code with `data-unresolved`. **The two are deliberately distinct:** a dissolved federation or
unrecognized territory in a longitudinal dataset is *data*, not a defect, so it renders the neutral
fallback (occupying its normal footprint in a table or card grid) rather than localized error
wording that reads to a user as a bug. Style the two states apart with those attributes.
The built-in `fallback-image` uses the same full-frame sizing, `object-fit`, circle clipping, and
forced-colors inset boundary as the resolved `image`; replacing it with slotted fallback content
leaves that content's presentation under the caller's control.

**Themeable custom properties:** `--lr-flag-radius` (default `calc(var(--lr-radius) * 0.33)` —
rectangular corner radius), `--lr-flag-aspect-ratio` (default `4 / 3`), and
`--lr-flag-object-fit` (default `cover`); also consumes `--lr-color-border` for the inset ring.

**Sizing.** The host has no intrinsic `width` — it sizes from `font-size` (`block-size: 1em`,
`inline-size` derived from `--lr-flag-aspect-ratio` via CSS `aspect-ratio`), so it scales naturally
with surrounding text (`<lr-flag style="font-size: 2rem">`). Setting `width`/`inline-size` directly
instead makes both axes definite, which defeats `aspect-ratio` (only applies when at most one axis
is definite) and squashes the image rather than scaling it.

**Optional peer deps:** `@aceshooting/lyra-flags` — required for the component to actually render an
image when `country` or `language` is used. Import
`@aceshooting/lyra-ui/components/media/flag/flag-peer.js` once to opt into
that resolver; a pre-resolved `src` works without the peer registration entry. If the peer is not
installed — or if `flag-peer.js` was simply never imported, which looks identical from the page —
the component fails closed with localized visible `[part="error"]` text, a shared light-DOM
assertive announcement, and a one-time `console.warn` naming the unresolved code and this import
(see gotchas).

Also exported from the package root:
`languageToCountry(language: string): string | undefined` and the `LANGUAGE_TO_COUNTRY` lookup
table (region subtag wins, e.g. `en-US` → `us`; plain `en` → `gb`; override the table per-app if you
need different defaults). Only the table's own entries are eligible for base-language fallback;
inherited object members such as `constructor` resolve to `undefined`. Also exported:
`localeNativeName(tag: string): string`.

`localeNativeName()` returns a BCP-47 tag's **endonym** — the locale's name written in that locale
itself (`'fr'` → `français`, `'pt-BR'` → `português (Brasil)`). That is what a language switcher
should list, so a reader who understands none of the current UI language can still find their own.
It derives from `Intl.DisplayNames`, so no name table ships with the library and results follow the
browser's own ICU data; the underlying instance comes from a shared memoized cache, since a picker
does one lookup per offered locale on every render pass. A tag with no display name resolves to the
tag itself, and so does a structurally invalid one — `Intl.DisplayNames` throws a `RangeError` on
those rather than falling back, and a language picker should degrade to showing the raw tag rather
than tearing down the render. Pair it with `languageToCountry()` for the flag half of the same row.

**Locale picker recipe.** `<lr-locale-picker>`
(`@aceshooting/lyra-ui/components/lr-locale-picker.js`) is the built-in
locale switcher — a closed-list dropdown over the locale registry or an explicit catalog, with
`lr-flag`/`localeNativeName()` rows and full form association out of the box. The manual
composition below remains available for an app that wants different chrome (its own dismiss
surface, a different active-state marker, or a layout `<lr-locale-picker>` doesn't offer):
`<lr-popover>` supplies the light-dismiss surface, `<lr-flag>` the country mark,
`localeNativeName()` the endonym, and `aria-current="true"` marks the active choice. Which locales
exist is the app's decision, so the app owns the list. Set `lang` on each row so assistive tech
pronounces the endonym in its own language, and use `fidelity="compact"` at icon scale.

```html
<lr-popover placement="bottom-start">
  <button slot="trigger">
    <lr-flag language="fr" label="" style="height: 1rem"></lr-flag>
    <span>français</span>
  </button>
  <ul role="list">
    <!-- one <li><button lang="pt-BR" aria-current="false"> … </button></li> per offered locale -->
  </ul>
</lr-popover>
```

```js
import {
  localeNativeName,
  languageToCountry,
} from "@aceshooting/lyra-ui/components/media/flag/language-map.js";

const rows = ["en", "fr", "de", "pt-BR", "ja", "ar"].map((tag) => ({
  tag,
  name: localeNativeName(tag), // endonym, e.g. "português (Brasil)"
  country: languageToCountry(tag), // flag code for the same row
}));
```

```html
<lr-flag country="fr" label="France"></lr-flag>
<lr-flag language="en-US" shape="circle"></lr-flag>
<lr-flag country="es" fidelity="compact"></lr-flag>
<!-- tiny WebP raster, icon-scale -->
<lr-flag country="es" fidelity="detailed"></lr-flag>
<!-- pristine full-fidelity vector -->
```

```bash
pnpm add @aceshooting/lyra-flags   # required for country/language lookup; failures render error text
```

```js
import "@aceshooting/lyra-ui/components/media/flag/flag-peer.js";
```

**Known gotchas:**

- `country`/`language` resolution is opt-in through
  `@aceshooting/lyra-ui/components/media/flag/flag-peer.js`; `all.js`
  registers the component without importing the optional flag asset graph. Requires the optional
  peer `@aceshooting/lyra-flags` to actually render an image; without it the component still shows a
  decorative `<lr-skeleton shape="rect" announce="false">` placeholder while resolving. The
  host exposes `aria-busy="true"`, and ordinary sr-only text preserves the localized `loading`
  label without creating a shadow live region. Resolution failure then **fails closed** into ordinary localized
  `<span part="error">` text (the `flagLoadError` message key, `"Flag unavailable"` by default).
  Each fresh failure appends that same localized message to the document's pre-mounted
  `[data-lr-live-region="assertive"]` sink, so the shadow chrome itself is not live and identical
  retries remain separate additions. The failure also produces a one-time `console.warn`
  once the resolver rejects (lazy `import()`, cached module-wide so the warning fires only once per
  page even with many `<lr-flag>` instances). A **separate** one-time `console.warn` covers the
  commonest setup mistake — `country`/`language` set while no resolver was ever registered, i.e.
  `flag-peer.js` was not imported. It names the offending code and that import path, because the
  visible `[part="error"]` state alone is indistinguishable from missing flag data. It is armed
  once per resolver-registration generation, so a table of many flags warns once.
  An _empty_ template is a different, non-error outcome:
  the peer resolved fine but returned no URL for that code (e.g. `country="zz"`) — no `[part="error"]`,
  no `<img>`, no warning.
- Rendering is async even when the peer _is_ installed: `src` resolves after an `import()` +
  resolver call, so there's a brief loading-skeleton window on first paint/attribute change — don't
  assume the `<img>` exists synchronously right after setting `country`/`language`.
- if both `aria-label` and `label` are omitted, the accessible name (`alt`) falls back to a localized
  region name via `Intl.DisplayNames([effectiveLocale], { type: 'region' })` (e.g. `"United Kingdom"`) instead of
  a bare code — for `language="en"` that's derived from `"GB"` (the mapped country), not `"EN"` (the
  language tag itself). Falls back further to the bare uppercase code if `Intl.DisplayNames` throws
  for an unrecognized region or isn't available. You can still pass an explicit `label` (e.g.
  `"France"`) to override the derived name.
- an invalid `country` (anything not matching the ISO 3166-1 alpha-2 shape, e.g. containing `../`)
  is rejected before it ever reaches the peer's `flagUrl()` resolver — treated the same as an
  unknown/missing flag rather than being passed through. `languageToCountry()`'s region-subtag path
  is validated against that same alpha-2 shape too (not just a bare length-2 check), so a malformed
  region such as `"en-01"` correctly falls through to the `LANGUAGE_TO_COUNTRY[base]` lookup instead
  of being accepted verbatim as a country code.
- rapidly reassigning `country`/`language`/`src` no longer risks a stale resolution overwriting a
  newer one: each resolver call is tagged with a token captured at the time it was kicked off, and a
  `.then()` that fires after a later change (or after the field was cleared) is discarded instead of
  clobbering the rendered image/`loading`.
- `country`/`language` resolve through `@aceshooting/lyra-flags`'s `flagUrl(code)`, which lazily
  fetches one requested flag at runtime. A bundler may still emit the complete reachable lazy-chunk
  graph because every supported code has a literal loader import; use a literal asset subpath
  import when the deployment artifact must be pruned. If every `<lr-flag>` in the app is pinned to
  one `fidelity` (no per-instance switching), register
  `@aceshooting/lyra-flags/standard`/`/compact`/`/detailed` via `setFlagUrlResolver()` instead of
  importing `flag-peer.js` (which always registers the full three-tier resolver) — the tier-specific
  entry excludes the other two tiers' generated loader maps from the reachable graph; see that
  package's README. If you already have a flag's URL at build
  time, `src` skips the peer-package round trip; native image loading still uses the same bounded
  loading/error transaction.
- Rendering many flags at once (a country table, a picker listing every locale): resolve every code
  up front with `@aceshooting/lyra-flags`'s `flagUrls()` (one call, returns `{code: url}` for all
  249 flags) and pass results through `src`, instead of letting each `<lr-flag>` instance
  independently call `flagUrl()` — skips one peer-resolution round trip per instance. Image fetches
  themselves are unaffected either way (each flag is a distinct asset; there is no sprite).
  Alternatively, import `@aceshooting/lyra-ui/components/media/flag/flag-peer-bulk.js` instead of
  the default `flag-peer.js` (never both) to get this automatically: it registers a resolver backed
  by one shared `flagUrls()` call, so every `<lr-flag>` on the page benefits without threading `src`
  by hand. Only worth it when the page renders most/all flags — a page with a handful pays an
  unneeded 249-entry fetch. Only the standard tier is bulk-fetched this way;
  `fidelity="compact"/"detailed"` on individual elements still resolves through its own lazy
  per-code loader.
- Rendering many flags at once **and** leaving every `<lr-flag>` on the default
  `fidelity="standard"`: import
  `@aceshooting/lyra-ui/components/media/flag/flag-peer-bulk-standard.js` instead of
  `flag-peer-bulk.js` (never more than one of the three peer entries — each
  `setFlagUrlResolver()` call replaces the previous resolver). It registers
  `@aceshooting/lyra-flags/standard`'s `createFlagUrlResolver()`, which is backed by the same
  standard-tier-only eager map as the root's, without statically importing the detailed and
  compact loader maps the root entry needs for its per-call `variant`. Those maps are what
  `flag-peer-bulk.js` pays for its batching: on a real production build with a 156-country flag
  column, routing bulk resolution through the package root emitted +65 detailed SVGs and +31
  compact WebPs — +15.8MB of assets no route rendered — swamping the chunk-count win the bulk path
  exists for. The tradeoff is the tier commitment: `fidelity="compact"/"detailed"` on an individual
  element resolves to that code's standard asset instead (a silent no-op, not an error), so use
  `flag-peer-bulk.js` when per-instance fidelity must actually be honoured.
- 65 of `@aceshooting/lyra-flags`' 249 flags (any whose design includes a detailed coat of
  arms/seal/emblem, e.g. `es`, `pt`, `sv`) ship **three** fidelity tiers, selected via the
  `fidelity` property (`flagUrl(code, { variant: fidelity })` under the hood): `"compact"` — a tiny WebP raster for
  icon-scale use (menus, language pickers, dense lists); `"standard"` — the default, the
  icon-optimized vector for card/row sizes, ~84% smaller on average than the pristine source for the
  65 affected codes with no visible fidelity loss at that scale; `"detailed"` — the pristine
  full-fidelity vector, for hero-scale display where the extra illustrative detail is actually
  visible. The other 184 codes resolve to the same file regardless of `fidelity` — a safe no-op.

**Additional API surface:**

- `part="error"` — Ordinary localized visible text rendered when the optional peer resolver is
  unavailable or fails; the fresh transition is announced by the shared light-DOM assertive sink.

---
