A lightweight, fully responsive, and accessible React package containing modal components and utilities for selecting Countries, Cities, and Phone Number Calling Codes.
Built from the ground up with TypeScript, Tailwind CSS, and Radix UI, react-geo-modals works seamlessly out-of-the-box in both Vite (Client SPAs) and Next.js (App & Pages Routers, Server/Client components).
- π Zero Configuration UI: Clean, modern modals styled consistently with Tailwind CSS and Radix primitives.
- π± Mobile First & Responsive: Fluid viewports (
95vw, optimized heights) ensuring no layout clipping on mobile devices or small screens. - π Framework Agnostic: Compatible with modern React frameworks supporting
"use client"directives natively. - π TypeScript First: Fully typed out of the box with strict generics, making form integration (like
react-hook-form) seamless. - π¦ Rich Built-In Datasets: Pre-bundled with comprehensive international country profiles, national flags, calling codes, and nested city datasets.
Install the package via your preferred package manager:
npm install react-geo-modals
# or
yarn add react-geo-modals
# or
pnpm add react-geo-modalsEnsure your host application has the following installed:
react(^18.0.0 || ^19.0.0)react-dom(^18.0.0 || ^19.0.0)lucide-react(used for UI icons)
The package includes compiled CSS, so Tailwind CSS is not required in the host application. Import the stylesheet once in your application entry file:
import 'react-geo-modals/style.css';If your application already uses Tailwind, you can use its own generated utilities
instead. Make sure your Tailwind setup scans the package output so the component
classes are included in your application CSS. For Tailwind v3, add the package to
content:
// tailwind.config.js
export default {
content: [
'./src/**/*.{js,ts,jsx,tsx}',
'./node_modules/react-geo-modals/dist/**/*.{js,ts}',
],
}With Tailwind v4, add the package source to your CSS using @source if your setup
does not scan dependencies automatically:
@import "tailwindcss";
@source "../node_modules/react-geo-modals/dist";Restart the Vite or Next.js development server after changing Tailwind scanning paths.
Allows users to pick a country name from an alphabetized, scrollable list complete with emoji flags and international codes.
import { useState } from 'react';
import { CountryModal } from 'react-geo-modals';
export default function CountrySelectDemo() {
const [isOpen, setIsOpen] = useState(false);
const [selectedCountry, setSelectedCountry] = useState('');
return (
<div className="w-full max-w-sm space-y-2">
<label className="text-sm font-medium text-foreground">Country</label>
<button
type="button"
onClick={() => setIsOpen(true)}
className="w-full flex items-center justify-between px-4 py-2.5 rounded-xl border border-border bg-background text-sm shadow-sm hover:bg-muted/50 transition-colors"
>
<span>{selectedCountry || 'Select a country...'}</span>
<span className="text-xs text-muted-foreground">βΌ</span>
</button>
<CountryModal
showCountryModal={isOpen}
setShowCountryModal={setIsOpen}
onChange={(country) => {
setSelectedCountry(country);
console.log('Selected country:', country);
}}
/>
</div>
);
}Dynamically renders the major cities associated with the selected parent country.
import { useState } from 'react';
import { CitiesModal } from "react-geo-modals";
export default function CitySelectDemo() {
const [isOpen, setIsOpen] = useState(false);
const [country] = useState('Canada'); // Can be bound to your state
const [selectedCity, setSelectedCity] = useState('');
return (
<div className="w-full max-w-sm space-y-2">
<label className="text-sm font-medium text-foreground">City ({country})</label>
<button
type="button"
onClick={() => setIsOpen(true)}
className="w-full flex items-center justify-between px-4 py-2.5 rounded-xl border border-border bg-background text-sm shadow-sm hover:bg-muted/50 transition-colors"
>
<span>{selectedCity || `Select city in ${country}...`}</span>
<span className="text-xs text-muted-foreground">βΌ</span>
</button>
<CitiesModal
showCitiesModal={isOpen}
setShowCitiesModal={setIsOpen}
country={country}
onChange={(city) => {
setSelectedCity(city);
}}
/>
</div>
);
}Perfect for onboarding flows, authentication inputs, and international telephone capture.
import { useState } from 'react';
import { PhoneNumberModal, formatPhoneNumber } from "react-geo-modals";
export default function PhoneInputDemo() {
const [isOpen, setIsOpen] = useState(false);
const [countryCode, setCountryCode] = useState('+1');
const [phoneNumber, setPhoneNumber] = useState('');
return (
<div className="w-full max-w-sm space-y-2">
<label className="text-sm font-medium text-foreground">Phone Number</label>
<div className="flex gap-2">
{/* Country Code Trigger Button */}
<button
type="button"
onClick={() => setIsOpen(true)}
className="px-3 py-2.5 rounded-xl border border-border bg-background text-sm font-semibold shadow-sm hover:bg-muted/50 transition-colors flex items-center gap-1.5"
>
<span>{countryCode}</span>
<span className="text-xs text-muted-foreground">βΌ</span>
</button>
{/* Input field utilizing formatPhoneNumber utility */}
<input
type="tel"
value={phoneNumber}
onChange={(e) => setPhoneNumber(formatPhoneNumber(e.target.value))}
placeholder="555-0199"
className="flex-1 px-4 py-2.5 rounded-xl border border-border bg-background text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-ring"
/>
</div>
<PhoneNumberModal
showPhoneNumberModal={isOpen}
setShowPhoneNumberModal={setIsOpen}
onChange={(code) => {
setCountryCode(code);
}}
/>
</div>
);
}import {
CitiesModal,
CountryModal,
PhoneNumberModal,
countries,
formatPhoneNumber,
} from 'react-geo-modals';CountryModal: select a country.CitiesModal: select a city for a country.PhoneNumberModal: select an international calling code.countries: the bundled country and city dataset.formatPhoneNumber: strips non-digits and limits input to 15 digits.
All modal components share an identical and predictable state-handling signature:
| Prop Name | Type | Description |
|---|---|---|
show[ModalName] |
boolean |
Controls the open/closed visibility state of the modal. |
setShow[ModalName] |
React.Dispatch<React.SetStateAction<boolean>> |
State dispatcher function to update visibility. |
onChange |
(value: string) => void |
Callback function triggered when an item is selected. Passes back the target value (country.name, city, or country.code). |
country (CitiesModal only) |
string |
The parent country string used to filter and lookup the respective cities array. |
formatPhoneNumber(value: string): string- Strips all non-digit characters from string input.
- Automatically bounds strings to a maximum of 15 digits conforming to international standard E.164.
If Vite reports Failed to resolve import "react-geo-modals", install the
package and restart the dev server:
npm install react-geo-modals@latest
npm run devDo not install or publish node_modules; npm installs the package's declared runtime
dependencies automatically.
- Clone the repository:
git clone https://github.com/kilamaelie/react-geo-modals.git cd react-geo-modals
2. **Install dependencies:**
```bash
npm install
- Run build watch mode:
npm run dev
Distributed under the MIT License. See LICENSE for more information.
Releases are published to npm through GitHub Actions using npm trusted publishing with OpenID Connect (OIDC). To configure the trust once, open the package's Trusted Publishers settings on npm and add:
- Repository:
kilamaelie/react-geo-modals - Workflow:
.github/workflows/publish.yml
After that, create and push a v* tag for the package version. The workflow
builds the package and publishes it with provenance without an npm token.