shadimage
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}`)
  }
}

More from flagcn

Browse all components in this library →