Let me share two methods for displaying a country flag with React.
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.
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.
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.
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.