lipis/flag-icons
A curated collection of 250+ SVG country flags, usable via CDN, npm, or simple CSS classes like fi fi-us. Each flag is a clean, optimized SVG with a consistent aspect ratio, making it easy to add crisp, scalable flags to any web project. Lightweight, dependency-free, and MIT licensed.
lipis/flag-icons is not a PHP package — it is a static asset bundle (SVG flags + a CSS file). There is nothing to composer require. You pull it in through your front-end pipeline, and Laravel's job is simply to render the right two classes.
1. Install via npm (recommended for Vite apps):
npm install flag-icons
2. Import the CSS once, in resources/css/app.css:
@import 'flag-icons/css/flag-icons.min.css';
Or in resources/sass/app.scss if you are on the SCSS stack:
@import 'flag-icons/sass/flag-icons';
3. Use it — the contract is two classes:
<span class="fi fi-us"></span> <!-- 4:3 United States -->
<span class="fi fis fi-de"></span> <!-- 1:1 Germany -->
fi is the base class (sets background-size: contain, display: inline-block, an em-based box). fi-<code> is the ISO 3166-1 alpha-2 code, lowercase. fis switches to the square (1:1) asset set.
First use case: a country/language switcher. Create a Blade component so the two-class rule never gets violated at call sites:
// app/View/Components/Flag.php
class Flag extends Component
{
public function __construct(
public string $code,
public bool $square = false,
public ?string $label = null,
public string $size = '', // xs|sm|lg|xl
) {
$this->code = strtolower($code);
}
}
{{-- resources/views/components/flag.blade.php --}}
<span
class="fi {{ $square ? 'fis' : '' }} fi-{{ $code }} {{ $size ? 'fi-'.$size : '' }}"
@if($label) role="img" aria-label="{{ $label }}" @else aria-hidden="true" @endif
></span>
Then everywhere: <x-flag code="gb" label="United Kingdom" />.
Where to look first: node_modules/flag-icons/css/flag-icons.css for the class contract, and node_modules/flag-icons/flags/4x3/ for the full list of available codes. Grep that directory rather than trusting an ISO list — it is the source of truth for what actually renders.
Store alpha-2 codes, not flag names. Your countries table should hold char(2) ISO codes. All display logic derives from that. A small accessor/normalizer saves a lot of pain:
public function getFlagCodeAttribute(): string
{
return match ($this->iso2) {
'UK' => 'gb', // people type UK; ISO says GB
null, '' => 'xx', // flag-icons ships a placeholder
default => strtolower($this->iso2),
};
}
xx and un and eu exist and are genuinely useful placeholders for "unknown", "international", and "EU" respectively.
Language vs. country is not the same thing. If you are building a locale switcher, do not map en → us. Either show the language name with no flag (safest, and what most mature products do), or maintain an explicit locale → flag map, because en could be gb, us, au, ie… and pt is pt or br. This is the single most common correctness bug with this package.
Sizing. Dimensions are em-based, so the cleanest control is font-size on a wrapper rather than fighting utility classes:
<span class="text-2xl"><x-flag code="jp" /></span>
The bundled .fi-xs .fi-sm .fi-lg .fi-xl modifiers work too, but they compose poorly with Tailwind's text scale — pick one system.
Selects and dropdowns. Flags in <option> elements are unreliable across browsers and OSes (native selects often ignore background images). Use a custom listbox (Alpine, Livewire, or Headless UI) with <x-flag> inside the option rows, and keep the native select for the no-JS fallback if you need one.
Pair with a name source. The package gives you a rectangle, not a label. Combine with laravel-countries, nations, or your own table for translated country names, dial codes, and currencies.
Only ship the flags you need. With the SCSS entry point you can tree-shake the CSS down to a handful and skip emitting ~500 SVGs through Vite:
// resources/sass/app.scss
$flag-icons-path: '../flags';
$flag-icons-included-countries: ('us', 'gb', 'de', 'fr', 'jp', 'br');
@import 'flag-icons/sass/flag-icons';
Vite + asset URLs. When you import the packaged CSS, Vite rewrites url(../flags/4x3/us.svg) and emits hashed copies into public/build/assets/. That works, but it produces hundreds of files. If your build budget matters, the alternative is to copy the folders into public/vendor/flag-icons/ (via a postinstall script or vite-plugin-static-copy) and override $flag-icons-path so the CSS points at a stable, unhashed path.
CDN escape hatch. For a marketing page, admin panel, or Blade-only project with no build step, skip npm:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flag-icons@7/flags/4x3/../css/flag-icons.min.css">
Pin the major version and add the host to your CSP style-src / img-src.
.fi alone renders an empty box. It is the frame; fi-<code> provides the image. And the base class is mandatory — class="fi-us" does nothing.
Codes are case-sensitive. .fi-US silently produces a blank rectangle with no console warning. Normalize to lowercase at the boundary (component constructor, cast, accessor) rather than trusting your inputs.
uk is not a flag. Use gb. Same trap: fi-en does not exist (that is a language). Users will type UK into forms and wonder why the flag vanishes.
fis and size modifiers interact. Square flags want class="fi fis fi-de" — but note the 1:1 assets are separately drawn, not centre-crops of the 4:3 art, so a few flags look meaningfully different between the two sets. Decide on one aspect ratio per surface and stick to it.
Subnational flags exist but are incomplete. gb-sct, gb-wls, gb-eng, gb-nir, es-ct, es-ga and a scattering of others are present. Do not assume full ISO 3166-2 coverage — always check the flags/4x3/ directory.
Flags are mostly unlabelled rectangles. On a dark background, <x-flag code="de" /> next to a dark UI element disappears. Add a subtle ring or border at the call site (class="rounded-sm ring-1 ring-black/10"), and never rely on the flag alone to convey meaning — pair it with text for accessibility. Screen readers get nothing from a background image, hence the role="img" aria-label in the component, or aria-hidden="true" when a text label is already adjacent.
Email clients will not show these. Most block background-image on <span>, and Gmail strips <style>. For transactional email, export the specific SVGs to PNG or inline the SVG paths — this package's CSS approach is a web-only solution.
Tailwind's content scanner is irrelevant here. fi-{{ $code }} looks dynamic, but flag-icons' CSS is shipped wholesale, so there is no purge risk with the default import. The risk appears only if you use $flag-icons-included-countries and then render codes at runtime that were not in the list — you get a silent blank. Safelist those codes in one place and reference the same constant from both the SCSS build and the app.
Vertical alignment. .fi is an inline-block with line-height: 1em, which sits awkwardly next to text. Wrap it (inline-flex items-center gap-2) and let flexbox handle alignment instead of nudging with relative -top-px.
No JS, no wrapper, no maintenance risk. That is the upside: there is nothing to keep in sync with Laravel versions, no service provider, no config to publish. The trade-off is that you own the integration code — which is why a single Blade component plus one normalizer is worth the ten minutes. Avoid the various third-party "Laravel flag" Composer wrappers; most just render these same two classes with an extra dependency and lag behind the CSS releases.
Debugging checklist when a flag does not appear: (1) is the CSS file actually in the built bundle — check public/build/manifest.json; (2) is the code lowercase and valid — ls node_modules/flag-icons/flags/4x3/<code>.svg; (3) did Vite rewrite the url() correctly — inspect the computed background-image in devtools; (4) if using the SCSS include-list, is the code in it.
How can I help you explore Laravel packages today?