Displaying Country Flags in React: Emojis vs SVG

September 5, 2023

7 min read

Displaying Country Flags in React: Emojis vs SVG
Watch on YouTube

Let me share two methods for displaying a country flag with React.

Country code

Before delving into these options, we need an object with a two-letter country code and its name.

export const countryNameRecord = {
  AF: "Afghanistan",
  AL: "Albania",
  DZ: "Algeria",
  AS: "American Samoa",
  AD: "Andorra",
  AO: "Angola",
  AI: "Anguilla",
  AQ: "Antarctica",
  AG: "Antigua and Barbuda",
  AR: "Argentina",
  AM: "Armenia",
  AW: "Aruba",
  AU: "Australia",
  AT: "Austria",
  AZ: "Azerbaijan",
  BS: "Bahamas",
  BH: "Bahrain",
  BD: "Bangladesh",
  BB: "Barbados",
  BY: "Belarus",
  BE: "Belgium",
  BZ: "Belize",
  BJ: "Benin",
  BM: "Bermuda",
  BT: "Bhutan",
  BO: "Bolivia",
  BA: "Bosnia and Herzegovina",
  BW: "Botswana",
  BV: "Bouvet Island",
  BR: "Brazil",
  IO: "British Indian Ocean Territory",
  BN: "Brunei",
  BG: "Bulgaria",
  BF: "Burkina Faso",
  BI: "Burundi",
  KH: "Cambodia",
  CM: "Cameroon",
  CA: "Canada",
  CV: "Cape Verde",
  KY: "Cayman Islands",
  CF: "Central African Republic",
  TD: "Chad",
  CL: "Chile",
  CN: "China",
  CX: "Christmas Island",
  CC: "Cocos (Keeling) Islands",
  CO: "Colombia",
  KM: "Comoros",
  CG: "Congo",
  CK: "Cook Islands",
  CR: "Costa Rica",
  HR: "Croatia",
  CU: "Cuba",
  CY: "Cyprus",
  CZ: "Czech Republic",
  DK: "Denmark",
  DJ: "Djibouti",
  DM: "Dominica",
  DO: "Dominican Republic",
  TP: "East Timor",
  EC: "Ecuador",
  EG: "Egypt",
  SV: "El Salvador",
  GQ: "Equatorial Guinea",
  ER: "Eritrea",
  EE: "Estonia",
  ET: "Ethiopia",
  FK: "Falkland Islands",
  FO: "Faroe Islands",
  FJ: "Fiji Islands",
  FI: "Finland",
  FR: "France",
  GF: "French Guiana",
  PF: "French Polynesia",
  TF: "French Southern territories",
  GA: "Gabon",
  GM: "Gambia",
  GE: "Georgia",
  DE: "Germany",
  GH: "Ghana",
  GI: "Gibraltar",
  GR: "Greece",
  GL: "Greenland",
  GD: "Grenada",
  GP: "Guadeloupe",
  GU: "Guam",
  GT: "Guatemala",
  GG: "Guernsey",
  GN: "Guinea",
  GW: "Guinea-Bissau",
  GY: "Guyana",
  HT: "Haiti",
  HM: "Heard Island and McDonald Islands",
  VA: "Holy See (Vatican City State)",
  HN: "Honduras",
  HK: "Hong Kong",
  HU: "Hungary",
  IS: "Iceland",
  IN: "India",
  ID: "Indonesia",
  IR: "Iran",
  IQ: "Iraq",
  IE: "Ireland",
  IM: "Isle of Man",
  IL: "Israel",
  IT: "Italy",
  CI: "Ivory Coast",
  JM: "Jamaica",
  JP: "Japan",
  JE: "Jersey",
  JO: "Jordan",
  KZ: "Kazakhstan",
  KE: "Kenya",
  KI: "Kiribati",
  KW: "Kuwait",
  KG: "Kyrgyzstan",
  LA: "Laos",
  LV: "Latvia",
  LB: "Lebanon",
  LS: "Lesotho",
  LR: "Liberia",
  LY: "Libyan Arab Jamahiriya",
  LI: "Liechtenstein",
  LT: "Lithuania",
  LU: "Luxembourg",
  MO: "Macao",
  MK: "North Macedonia",
  MG: "Madagascar",
  MW: "Malawi",
  MY: "Malaysia",
  MV: "Maldives",
  ML: "Mali",
  MT: "Malta",
  MH: "Marshall Islands",
  MQ: "Martinique",
  MR: "Mauritania",
  MU: "Mauritius",
  YT: "Mayotte",
  MX: "Mexico",
  FM: "Micronesia, Federated States of",
  MD: "Moldova",
  MC: "Monaco",
  MN: "Mongolia",
  ME: "Montenegro",
  MS: "Montserrat",
  MA: "Morocco",
  MZ: "Mozambique",
  MM: "Myanmar",
  NA: "Namibia",
  NR: "Nauru",
  NP: "Nepal",
  NL: "Netherlands",
  NC: "New Caledonia",
  NZ: "New Zealand",
  NI: "Nicaragua",
  NE: "Niger",
  NG: "Nigeria",
  NU: "Niue",
  NF: "Norfolk Island",
  KP: "North Korea",
  GB: "United Kingdom of Great Britain and Northern Ireland",
  MP: "Northern Mariana Islands",
  NO: "Norway",
  OM: "Oman",
  PK: "Pakistan",
  PW: "Palau",
  PS: "Palestine",
  PA: "Panama",
  PG: "Papua New Guinea",
  PY: "Paraguay",
  PE: "Peru",
  PH: "Philippines",
  PN: "Pitcairn",
  PL: "Poland",
  PT: "Portugal",
  PR: "Puerto Rico",
  QA: "Qatar",
  RE: "Reunion",
  RO: "Romania",
  RU: "Russian Federation",
  RW: "Rwanda",
  SH: "Saint Helena",
  KN: "Saint Kitts and Nevis",
  LC: "Saint Lucia",
  PM: "Saint Pierre and Miquelon",
  VC: "Saint Vincent and the Grenadines",
  WS: "Samoa",
  SM: "San Marino",
  ST: "Sao Tome and Principe",
  SA: "Saudi Arabia",
  SN: "Senegal",
  RS: "Serbia",
  SC: "Seychelles",
  SL: "Sierra Leone",
  SG: "Singapore",
  SK: "Slovakia",
  SI: "Slovenia",
  SB: "Solomon Islands",
  SO: "Somalia",
  ZA: "South Africa",
  GS: "South Georgia and the South Sandwich Islands",
  KR: "South Korea",
  SS: "South Sudan",
  ES: "Spain",
  LK: "Sri Lanka",
  SD: "Sudan",
  SR: "Suriname",
  SJ: "Svalbard and Jan Mayen",
  SZ: "Swaziland",
  SE: "Sweden",
  CH: "Switzerland",
  SY: "Syria",
  TJ: "Tajikistan",
  TZ: "Tanzania",
  TH: "Thailand",
  CD: "The Democratic Republic of Congo",
  TL: "Timor-Leste",
  TG: "Togo",
  TK: "Tokelau",
  TO: "Tonga",
  TT: "Trinidad and Tobago",
  TN: "Tunisia",
  TR: "Turkey",
  TM: "Turkmenistan",
  TC: "Turks and Caicos Islands",
  TV: "Tuvalu",
  UG: "Uganda",
  UA: "Ukraine",
  AE: "United Arab Emirates",
  US: "United States",
  UM: "United States Minor Outlying Islands",
  UY: "Uruguay",
  UZ: "Uzbekistan",
  VU: "Vanuatu",
  VE: "Venezuela",
  VN: "Vietnam",
  VG: "Virgin Islands, British",
  VI: "Virgin Islands, U.S.",
  WF: "Wallis and Futuna",
  EH: "Western Sahara",
  YE: "Yemen",
  ZM: "Zambia",
  ZW: "Zimbabwe",
} as const

export type CountryCode = keyof typeof countryNameRecord

Subsequently, we can create a CountryCode type that will serve as a union of all country codes.

Use Emoji to Display a Flag

As there is an emoji for every flag, utilizing them would be the easiest choice for displaying the country flag.

import {
  CountryCode,
  countryNameRecord,
} from "@radzionkit/utils/countryNameRecord"
import { getCountryFlagEmoji } from "@radzionkit/utils/getCountryFlagEmoji"

interface CountryFlagEmojiProps {
  code?: CountryCode
}

export const CountryFlagEmoji = ({ code }: CountryFlagEmojiProps) => {
  const title = code ? countryNameRecord[code] || code : undefined
  return (
    <span role="img" aria-labelledby={title} title={title}>
      {code ? getCountryFlagEmoji(code) : "🏳"}
    </span>
  )
}

The CountryFlagEmoji component receives a single property - a country code. We make this parameter optional to accommodate scenarios where we need a flag shape placeholder in our UI. If the country code is present, we take the country's name from the countryNameRecord and pass it to aria-labelledby for accessibility purposes, also to the title attribute for the tooltip. We use the getCountryFlagEmoji function to generate a flag emoji from the country code.

import { CountryCode } from "./countryNameRecord"

export const getCountryFlagEmoji = (countryCode: CountryCode) => {
  const codePoints = countryCode
    .toUpperCase()
    .split("")
    .map((char) => 127397 + char.charCodeAt(0))
  return String.fromCodePoint(...codePoints)
}

We can now iterate over every country present in our record and evaluate the efficiency of an emoji flag as a solution.

CountryFlagEmoji
CountryFlagEmoji

Flag emojis are relatively effortless to implement, and they cover every country. However, they tend to appear differently across different platforms. This is the primary reason I opted for SVG flags when building the scoreboard with the most productive users for my productivity app - Increaser.

Use SVG to Display a Flag

The CountryFlag component receives the same code property as our emoji component. Yet, it also accepts an optional path to hosted SVG files. I employ a NextJS app, therefore I store all flags in /images/flags within the public folder. I located these flags in the flag-icons GitHub repo and copied them to my project.

import styled from "styled-components"
import { SafeImage } from "../ui/SafeImage"
import { CoverImage } from "../ui/images/CoverImage"
import { getColor } from "../ui/theme/getters"
import { centerContentCSS } from "../ui/utils/centerContentCSS"
import { UIComponentProps } from "../props"
import {
  CountryCode,
  countryNameRecord,
} from "@radzionkit/utils/countryNameRecord"

interface CountryFlagProps extends UIComponentProps {
  code?: CountryCode
  source?: string
}

const Wrapper = styled.div`
  aspect-ratio: 4 / 3;
  background: ${getColor("mist")};
  overflow: hidden;
  ${centerContentCSS};
`

export const CountryFlag = ({
  code,
  source = "/images/flags",
  ...props
}: CountryFlagProps) => {
  return (
    <Wrapper
      title={code ? countryNameRecord[code] || code : undefined}
      {...props}
    >
      {code && (
        <SafeImage
          src={code ? `${source}/${code.toLowerCase()}.svg` : undefined}
          render={(props) => (
            <CoverImage
              alt={countryNameRecord[code] || code}
              title={countryNameRecord[code] || code}
              {...props}
            />
          )}
        />
      )}
    </Wrapper>
  )
}

In order to outline the shape of a flag we render it inside a Wrapper, which is a component with a 4/3 aspect ratio. The Wrapper also has a mist background, hidden overflow, and flex display with centered content.

import { ReactNode } from "react"
import { useBoolean } from "../shared/hooks/useBoolean"

interface RenderParams {
  src: string
  onError: () => void
}

interface Props {
  src?: string
  fallback?: ReactNode
  render: (params: RenderParams) => void
}

export const SafeImage = ({ fallback = null, src, render }: Props) => {
  const [isFailedToLoad, { set: failedToLoad }] = useBoolean(false)

  return (
    <>
      {isFailedToLoad || !src
        ? fallback
        : render({ onError: failedToLoad, src })}
    </>
  )
}

To prevent a broken image scenario in the event of a failed load, we employ the SafeImage wrapper. This will restrain from rendering the image if an error is detected.

import styled from "styled-components"

export const CoverImage = styled.img`
  width: 100%;
  height: 100%;
  object-fit: cover;
`

The CoverImage component occupies the entire width and height of its parent. It applies object-fit: cover to ensure full area coverage by the image.

CountryFlag
CountryFlag