flagcn
uiflagsource: ok · refreshed
Flag
An accessible no-crop flag image primitive with 4:3, 1:1, and original frames in SVG, PNG, WebP, or JPEG.
npx shadcn add "https://flagcn.dev/r/flag.json"free
No selection tags yet — empty means no evidence, not lack of support.
Files2 · source cached 10/8/2026, 1:26:27 AM
src/components/flags/flag.tsx
// SPDX-License-Identifier: MIT
import * as React from "react"
import {
getFlagSrcSet,
getFlagUrl,
type FlagFormat,
type FlagRatio,
type FlagWidth,
} from "./flag-utils"
export interface FlagProps
extends Omit<React.ComponentProps<"img">, "src" | "srcSet" | "width" | "height"> {
code: string
format?: FlagFormat
width?: FlagWidth | number
ratio?: FlagRatio
decorative?: boolean
}
export function Flag({
code,
format = "svg",
width = 80,
ratio = "4x3",
decorative = false,
alt,
loading = "lazy",
decoding = "async",
sizes,
className,
style,
...props
}: FlagProps) {
const src = getFlagUrl(code, { format, width, ratio })
const srcSet = getFlagSrcSet(code, { format, width, ratio })
const normalizedCode = code.trim().toLowerCase()
const height = ratio === "original" ? undefined : ratio === "1x1" ? width : Math.round(width * 0.75)
const aspectRatio = ratio === "original" ? undefined : ratio === "1x1" ? "1 / 1" : "4 / 3"
return (
<img
{...props}
data-slot="flag"
data-code={normalizedCode}
data-format={format}
data-ratio={ratio}
src={src}
srcSet={srcSet}
sizes={sizes ?? (srcSet ? `${width}px` : undefined)}
width={width}
height={height}
alt={decorative ? "" : (alt ?? `${code.toUpperCase()} flag`)}
aria-hidden={decorative || undefined}
loading={loading}
decoding={decoding}
className={className}
style={{
display: "block",
maxWidth: "100%",
...(aspectRatio ? {
aspectRatio,
objectFit: "contain" as const,
objectPosition: "center" as const,
} : {}),
...style,
}}
/>
)
}
src/components/flags/flag-utils.ts
// SPDX-License-Identifier: MIT
export const flagFormats = ["svg", "png", "webp", "jpg"] as const
export type FlagFormat = (typeof flagFormats)[number]
export const flagIconsVersion = "7.5.0" as const
export const flagIconsBaseUrl = `https://cdn.jsdelivr.net/npm/flag-icons@${flagIconsVersion}/flags`
export const flagWidths = [20, 40, 80, 160, 320, 640, 1280, 2560] as const
export type FlagWidth = (typeof flagWidths)[number]
export const flagRatios = ["4x3", "1x1", "original"] as const
export type FlagRatio = (typeof flagRatios)[number]
export interface FlagUrlOptions {
format?: FlagFormat
width?: FlagWidth | number
ratio?: FlagRatio
}
export interface FlagAssetInfo {
provider: "Flag Icons" | "FlagCDN"
kind: "vector" | "raster"
sourceProportions: boolean
label: string
}
export function getFlagAssetInfo(code: string, options: FlagUrlOptions = {}): FlagAssetInfo {
const normalizedCode = normalizeFlagCode(code)
const format = options.format ?? "svg"
const ratio = options.ratio ?? "4x3"
const usesFlagIcons = format === "svg" && ratio !== "original" && !normalizedCode.startsWith("us-")
const provider = usesFlagIcons ? "Flag Icons" : "FlagCDN"
const kind = format === "svg" ? "vector" : "raster"
const sourceProportions = !usesFlagIcons
return {
provider,
kind,
sourceProportions,
label: `${provider} · ${format.toUpperCase()}${ratio === "original" ? " · source proportions" : ""}`,
}
}
export function normalizeFlagCode(code: string) {
const normalizedCode = code.trim().toLowerCase()
if (!/^[a-z]{2}(?:-[a-z0-9]{2,3})?$/.test(normalizedCode)) {
throw new Error(`Invalid flag code: ${code}`)
}
return normalizedCode
}
export function getFlagUrl(code: string, options: FlagUrlOptions = {}) {
const normalizedCode = normalizeFlagCode(code)
const format = options.format ?? "svg"
const width = options.width ?? 80
const ratio = options.ratio ?? "4x3"
validateFlagWidth(width)
if (format === "svg") {
if (ratio !== "original" && !normalizedCode.startsWith("us-")) {
return `${flagIconsBaseUrl}/${ratio}/${normalizedCode}.svg`
}
return `https://flagcdn.com/${normalizedCode}.svg`
}
return `https://flagcdn.com/w${atLeastWidth(width, flagWidths)}/${normalizedCode}.${format}`
}
export function getFlagSrcSet(code: string, options: FlagUrlOptions = {}) {
const format = options.format ?? "svg"
if (format === "svg") return undefined
const ratio = options.ratio ?? "4x3"
const requestedWidth = options.width ?? 80
validateFlagWidth(requestedWidth)
const candidates = [requestedWidth, requestedWidth * 2, requestedWidth * 3]
.map((candidate) => atLeastWidth(candidate, flagWidths))
.filter((candidate, index, values) => values.indexOf(candidate) === index)
return candidates
.map((candidate) => `${getFlagUrl(code, { ...options, width: candidate, ratio })} ${candidate}w`)
.join(", ")
}
function atLeastWidth<T extends number>(width: number, widths: readonly T[]): T {
return widths.find((candidate) => candidate >= width) ?? widths.at(-1)!
}
function validateFlagWidth(width: number) {
if (!Number.isFinite(width) || width <= 0) {
throw new Error(`Flag width must be a positive finite number: ${width}`)
}
}