Rushan

Build a list view component in React with GeoIcons

  • react
  • country-icons
  • listview
  • accessibility
  • geoicons

When building dashboards, settings screens, or admin panels, a scrollable list of countries is a pattern you'll reach for again and again. However, a plain text list gives the reader no visual anchor: every row looks the same, and scanning for one country means reading every label. If you want each row to carry the country's map shape alongside its name, you render an icon per item.

In this tutorial, we'll build a beautiful, accessible country list view in React using GeoIcons, a premium library of geographic map icons.

What we'll build

  • A country list where each row pairs an ISO code, a name, and its icon component.
  • A semantic list that renders a map icon beside every item.
  • Selectable rows with the ARIA and keyboard behavior a plain <ul> doesn't give you.

Step 1: Install GeoIcons

First, add the React package to your project:

npm i @geoicons/react

It ships as ES modules with no runtime dependencies beyond React, so your bundler keeps only the icons you import. If you want the details, see how tree-shaking keeps the bundle small.

Step 2: Build the country data

Our list needs an array of items, and each item pairs an ISO code, a display name, and an icon. Import each country by its ISO 3166 alpha-2 code in PascalCase, then collect them into an array:

import { Us, Gb, Fr, De, Jp, Br, In, Ca, Au, Za } from '@geoicons/react/countries';
import type { ComponentType, SVGProps } from 'react';

// Matches the prop type each GeoIcon accepts (strokeWidth is a number, not string).
type IconProps = Omit<SVGProps<SVGSVGElement>, 'strokeWidth'> & {
  size?: number | string;
  strokeWidth?: number;
};

type Country = {
  code: string;
  name: string;
  Icon: ComponentType<IconProps>;
};

const countries: Country[] = [
  { code: 'us', name: 'United States', Icon: Us },
  { code: 'gb', name: 'United Kingdom', Icon: Gb },
  { code: 'fr', name: 'France', Icon: Fr },
  { code: 'de', name: 'Germany', Icon: De },
  { code: 'jp', name: 'Japan', Icon: Jp },
  { code: 'br', name: 'Brazil', Icon: Br },
  { code: 'in', name: 'India', Icon: In },
  { code: 'ca', name: 'Canada', Icon: Ca },
  { code: 'au', name: 'Australia', Icon: Au },
  { code: 'za', name: 'South Africa', Icon: Za },
];

You own this array, so it holds exactly the countries your product ships. Notice there's no <Icon name="us" /> lookup in GeoIcons, and that's deliberate: a string API would force the library to keep all 255 country icons in your bundle. Listing the icons yourself keeps the imports honest. For the reasoning, see rendering a country icon at runtime.

Step 3: Render the list

A list view is a semantic <ul> where each <li> renders a country's icon next to its name. Map over the array and render a row per country:

export function CountryList() {
  return (
    <ul className="divide-y rounded-md border">
      {countries.map((country) => {
        const Icon = country.Icon;
        return (
          <li key={country.code} className="flex items-center gap-3 px-3 py-2">
            <Icon size={24} aria-hidden="true" />
            <span>{country.name}</span>
          </li>
        );
      })}
    </ul>
  );
}

Each icon gets aria-hidden because the name beside it already conveys the country, so a screen reader announces it once rather than twice. The rendered list:

United States

United Kingdom

France

Germany

Japan

Because GeoIcons color from currentColor by default, each map icon inherits the text color of its row. Drop the list into a dark panel and the icons follow along, with no per-row color prop to manage.

Demo

Want to run it? Open the full project in StackBlitz, edit the country array, and see the list update live.

Open this example in StackBlitz

Add a trailing value

List views often show a value on the right: a count, a percentage, a dial code. Push it to the end of the row with justify-between:

<li className="flex items-center gap-3 px-3 py-2">
  <Icon size={24} aria-hidden="true" />
  <span className="flex-1">{country.name}</span>
  <span className="text-gray-500">{country.value}</span>
</li>

Add the field to your Country type (value: string) and populate it in the array. The icon and name stay on the left, and the value sits flush right.

Make rows selectable

A static list is fine for display. The moment a row does something on click, it stops being plain text and needs interactive semantics. Turn each row into a button and track the selection:

import { useState } from 'react';

const [selected, setSelected] = useState<string | null>(null);

<ul role="listbox" aria-label="Countries" className="divide-y rounded-md border">
  {countries.map((country) => {
    const Icon = country.Icon;
    return (
      <li
        key={country.code}
        role="option"
        aria-selected={country.code === selected}
        onClick={() => setSelected(country.code)}
        className="flex cursor-pointer items-center gap-3 px-3 py-2 aria-selected:bg-blue-50"
      >
        <Icon size={24} aria-hidden="true" />
        <span>{country.name}</span>
      </li>
    );
  })}
</ul>

Selectable rows carry keyboard duties too:

  • Roles: the list sets role="listbox" with an aria-label, and each row sets role="option" and aria-selected.
  • Keyboard: Arrow Down and Arrow Up move the active row, and Enter or Space selects it.
  • Focus: manage a roving tabIndex so one row is tabbable and the arrows move focus between rows.

Getting the roving focus right takes care, so for production it's worth reaching for a headless control that already implements the ARIA listbox pattern: Headless UI, Radix UI, or React Aria. You render a GeoIcon in each row, and the library owns the keyboard model.

Under the hood, each GeoIcon namespaces its <title> id with React's useId(), so rendering the same map icon on many rows never produces duplicate ids on the page.

Beyond countries: region lists

The same pattern builds a region list. Area icons live on a separate subpath and use a PascalCase slug instead of an ISO code:

import { EuropeanUnionEu } from '@geoicons/react/areas';

const regions = [
  { slug: 'eu', name: 'European Union', Icon: EuropeanUnionEu },
  // 167 areas: continents, landforms, groupings
];

Swap countries for regions in the component, and the list now shows continents and economic areas with the same rows and selection code.

FAQ

How do I render a country map icon in each list row?
Import each country by its ISO 3166 alpha-2 code from @geoicons/react/countries, store the components in an array of { code, name, Icon } records, and render the Icon inside each <li>. Give the icon aria-hidden since the row's name already conveys the country.
Should list rows be a ul or a listbox?
Use a plain semantic <ul>/<li> for a display-only list. When rows respond to clicks or selection, switch to role="listbox" on the list and role="option" with aria-selected on each row, and add arrow-key navigation.
How many icons does the list add to the bundle?
One per imported country. A list with ten countries imports ten icons. A list with all 255 countries imports 255. Bundlers prune any export the code does not reference.

Wrapping up

You now have an accessible country list view that renders a map icon on every row. The minimal setup is:

npm i @geoicons/react
import { Us, Fr, Jp } from '@geoicons/react/countries';

const countries = [
  { code: 'us', name: 'United States', Icon: Us },
  { code: 'fr', name: 'France', Icon: Fr },
  { code: 'jp', name: 'Japan', Icon: Jp },
];

From here, add a trailing value, or make the rows selectable with a headless library. For the full prop and import reference, see the GeoIcons API reference, or browse all 422 icons to find the countries you need. New to the library? Start with how to add country icons to a React app.