Country Phone Picker
A Flutter package for selecting countries with phone codes. It provides a bottom sheet picker with country flags, localized names, and dial codes — useful for phone number input forms.
Features
- Country list with ISO codes, dial codes, and flags
- Bottom sheet picker UI
- Built-in search by country name, dial code, or ISO code
- Localized country names (69 languages)
- Phone length and valid starting digits per country
- Set the initial country with an ISO code (
initialCountryCode: 'US') - Hide the search field (
showSearch: false) - Limit the list to an ISO allowlist (
allowedCountryCodes: ['JO', 'SA']) - Simple
CountryPhonePickerwidget for quick integration - Performance: country data is parsed once, search runs only when the query changes, and keyboard animation does not rebuild the list
Installation
Add this to your pubspec.yaml:
dependencies:
country_phone_picker: ^0.1.2
flutter_localizations:
sdk: flutter
Then run:
flutter pub get
Migrating from 0.0.2
Replace a full CountryModel with its ISO code. Use findCountryByIsoCode if you still need the model for hints or validation:
// Before
CountryPhonePicker(
initialCountry: myCountryModel,
...
)
// After
CountryPhonePicker(
initialCountryCode: 'US',
...
)
Basic Usage
1. Setup localization
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:country_phone_picker/country_phone_picker.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
supportedLocales: const [
Locale('en'),
Locale('ar'),
Locale('es'),
Locale('fr'),
],
localizationsDelegates: const [
CountryPickerLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
home: const HomePage(),
);
}
}
2. Use the picker
bottomSheetTitle and onChanged are required.
CountryPhonePicker(
bottomSheetTitle: 'Choose Country',
initialCountryCode: 'US',
bottomSheetConfig: BottomSheetConfig(
closeIcon: Image.asset(
'assets/icons/close.png',
width: 20,
height: 20,
),
titleStyle: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
radioActiveColor: Colors.deepPurple,
searchConfig: const SearchConfig(hintText: 'Search country'),
),
onChanged: (CountryModel country) {
print(country.name);
print(country.dialCode);
print(country.isoCode);
},
)
Complete Example
import 'package:flutter/material.dart';
import 'package:country_phone_picker/country_phone_picker.dart';
class PhoneInputExample extends StatefulWidget {
const PhoneInputExample({super.key});
@override
State<PhoneInputExample> createState() => _PhoneInputExampleState();
}
class _PhoneInputExampleState extends State<PhoneInputExample> {
static const String _initialCountryCode = 'US';
CountryModel selectedCountry =
findCountryByIsoCode(_initialCountryCode) ?? CountryModel.initialModel();
final TextEditingController phoneController = TextEditingController();
@override
void dispose() {
phoneController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Phone Number Input')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
CountryPhonePicker(
bottomSheetTitle: 'Choose Country',
initialCountryCode: _initialCountryCode,
onChanged: (CountryModel country) {
setState(() => selectedCountry = country);
},
),
const SizedBox(width: 8),
Expanded(
child: TextField(
controller: phoneController,
keyboardType: TextInputType.phone,
decoration: InputDecoration(
hintText: selectedCountry.hintText ?? 'Phone number',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
),
],
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () {
final fullNumber =
'${selectedCountry.dialCode}${phoneController.text}';
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Phone number: $fullNumber')),
);
},
child: const Text('Submit'),
),
],
),
),
);
}
}
See the full working app in the example directory:
cd example
flutter run
API Reference
CountryPhonePicker
| Property | Type | Required | Description |
|---|---|---|---|
onChanged |
ValueChanged<CountryModel> |
Yes | Called when a country is selected |
bottomSheetTitle |
String |
Yes | Title shown at the top of the bottom sheet |
bottomSheetConfig |
BottomSheetConfig |
No | Bottom sheet appearance; defaults to the standard package design |
initialCountryCode |
String? |
No | ISO 3166-1 alpha-2 code (e.g. "US") shown before the user picks one. Case-insensitive. null, empty, and unknown codes default to Jordan |
showSearch |
bool |
No | Whether the search field is shown. Defaults to true. The field is hidden when this is false or when SearchConfig.enabled is false |
allowedCountryCodes |
List<String>? |
No | ISO codes to keep in the list (e.g. ['JO', 'SA']). Case-insensitive. null or empty shows every country. Unknown codes are skipped. If none match, the full list is shown |
key |
Key? |
No | Widget key |
BottomSheetConfig
An optional immutable configuration for the modal, header, search field, country rows, flags, radio controls, text, spacing, and separator. All properties have defaults, so you only need to provide the values you want to change.
The closeIcon accepts any widget. Assets are resolved from the host
application, while the package keeps ownership of the dismiss action.
The sheet opens at initialHeightFactor of the available height instead of
taking the whole screen. Tapping the search field grows it to
expandedHeightFactor first, and the keyboard is only requested once that
animation completes, so the field never slides under the keyboard. Closing the
keyboard shrinks the sheet back to initialHeightFactor.
When the sheet opens, the list scrolls to the country that is currently selected and centers it, so the previous choice is visible without scrolling.
| Property | Type | Default | Description |
|---|---|---|---|
initialHeightFactor |
double |
0.6 |
Height fraction used when the sheet opens |
expandedHeightFactor |
double |
0.95 |
Height fraction used after the search field is tapped |
expandOnSearchTap |
bool |
true |
Grow the sheet before showing the keyboard |
expandDuration |
Duration |
250ms |
Duration of the growth animation |
expandCurve |
Curve |
Curves.easeOutCubic |
Curve of the growth animation |
scrollToSelected |
bool |
true |
Scroll to the selected country when the sheet opens |
scrollToSelectedAlignment |
double |
0.5 |
Position of that country in the list: 0 top, 0.5 center, 1 bottom |
Set expandOnSearchTap: false to keep a fixed height and open the keyboard on
the first tap. Set scrollToSelected: false to always open at the top of the
list.
CountryPhonePickerBottomSheetConfigstill works as a deprecated alias ofBottomSheetConfig.
SearchConfig
Controls the search field shown between the header and the country list. It is
enabled by default and filters on the localized country name, the fallback
package name, the ISO code, and the dial code (962, +962, and + 962 all
match Jordan).
| Property | Type | Default | Description |
|---|---|---|---|
enabled |
bool |
true |
Whether the search field is shown |
autofocus |
bool |
false |
Focus the field when the sheet opens |
matcher |
CountrySearchMatcher? |
null |
Replaces the default filtering logic |
padding |
EdgeInsetsGeometry |
horizontal: 16 |
Padding around the field |
bottomSpacing |
double |
8 |
Space between the field and the list |
contentPadding |
EdgeInsetsGeometry? |
null |
Padding inside the field |
hintText |
String |
'Search' |
Placeholder text |
textStyle / hintStyle |
TextStyle? |
null |
Query and hint styles |
cursorColor |
Color? |
null |
Cursor color |
keyboardType |
TextInputType |
TextInputType.text |
Keyboard type |
textInputAction |
TextInputAction |
TextInputAction.search |
Keyboard action |
prefixIcon |
Widget? |
Icon(Icons.search) |
Leading widget, accepts host assets |
showClearButton |
bool |
true |
Show a clear button while typing |
clearIcon |
Widget |
Icon(Icons.clear) |
Clear button content |
filled / fillColor |
bool / Color? |
false / null |
Field background |
border / enabledBorder / focusedBorder |
InputBorder? |
null |
Field borders |
decoration |
InputDecoration? |
null |
Complete decoration override |
emptyResultText |
String |
'No countries found' |
Message when nothing matches |
emptyResultStyle |
TextStyle? |
null |
Style of the empty message |
emptyResultBuilder |
WidgetBuilder? |
null |
Replaces the empty message widget |
CountryPhonePicker(
bottomSheetTitle: 'Choose Country',
bottomSheetConfig: BottomSheetConfig(
initialHeightFactor: 0.6,
expandedHeightFactor: 0.95,
searchConfig: SearchConfig(
hintText: 'Search country or code',
autofocus: true,
filled: true,
fillColor: Colors.deepPurple.shade50,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
emptyResultText: 'No match',
),
),
onChanged: (CountryModel country) {},
)
To hide the field, or to filter with your own rules:
CountryPhonePicker(
showSearch: false,
...
);
const SearchConfig(enabled: false);
SearchConfig(
matcher: (CountryModel country, String query) =>
country.dialCode.contains(query),
);
To show only some countries:
CountryPhonePicker(
allowedCountryCodes: ['JO', 'SA'],
bottomSheetTitle: 'Choose Country',
onChanged: (CountryModel country) {},
);
Unknown codes in that list are skipped. If the list is null, empty, or contains no supported countries, every country is shown.
countriesForIsoCodes
Resolves the same allowlist used by the picker. Returns the full country list when the input is null, empty, or has no supported codes.
final gulf = countriesForIsoCodes(['SA', 'AE', 'XX']);
CountryModel
| Property | Type | Description |
|---|---|---|
code |
int |
Internal country id |
name |
String |
Fallback country name from package data |
isoCode |
String |
ISO 3166-1 alpha-2 code (e.g. "US") |
dialCode |
String |
Dial code (e.g. "+1") |
hintText |
String? |
Phone format hint (e.g. "77-XXXXXXX") |
lengthNumber |
int |
Expected national number length |
phoneStartsWith |
List<String> |
Valid starting digit prefixes |
Factories
CountryModel.initialModel()— default country (Jordan /JO/+962)CountryModel.fromJson(Map<String, dynamic> json)— create from a map
findCountryByIsoCode
Looks up a country from the package list by ISO 3166-1 alpha-2 code (case-insensitive). Returns null if the code is unknown.
Use the same code you pass to CountryPhonePicker so the rest of your UI (hint, length, prefixes) matches the flag on first frame:
const iso = 'US';
final country = findCountryByIsoCode(iso) ?? CountryModel.initialModel();
CountryPhonePicker(
bottomSheetTitle: 'Choose Country',
initialCountryCode: iso,
onChanged: (next) { /* ... */ },
);
TextField(
decoration: InputDecoration(
hintText: country.hintText ?? 'Phone number',
),
);
To show a localized name in your own UI:
final name = CountryPickerLocalizations.of(context)
?.translate(country.isoCode) ??
country.name;
CountryPhonePickerBottomSheet
Public bottom sheet widget if you want to present the list yourself.
| Property | Type | Required | Description |
|---|---|---|---|
selectedCountryCode |
CountryModel |
Yes | Currently selected country |
bottomSheetTitle |
String |
Yes | Sheet title |
config |
BottomSheetConfig |
No | Bottom sheet appearance |
showSearch |
bool |
No | Whether the search field is shown. Defaults to true |
allowedCountryCodes |
List<String>? |
No | ISO allowlist used for the sheet list |
Localization
Register the delegate and locales you need:
localizationsDelegates: const [
CountryPickerLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
Force English names
localizationsDelegates: [
CountryPickerLocalizations.getDelegate(enableLocalization: false),
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
Supported language codes (69)
af, am, ar, az, be, bg, bn, bs, ca, cs, da, de, el, en, es, et, fa, fi, fr, gl, ha, he, hi, hr, hu, hy, id, is, it, ja, ka, kk, km, ko, ku, ky, lt, lv, mk, ml, mn, ms, nb, nl, nn, no, pl, ps, pt, ro, ru, sd, sk, sl, so, sq, sr, sv, ta, tg, th, tr, tt, ug, uk, ur, uz, vi, zh
Phone number validation helpers
CountryPhonePicker(
bottomSheetTitle: 'Choose Country',
onChanged: (CountryModel country) {
final isValidLength = phoneNumber.length == country.lengthNumber;
final hasValidStart = country.phoneStartsWith.any(
(prefix) => phoneNumber.startsWith(prefix),
);
if (isValidLength && hasValidStart) {
// Valid against package rules
}
},
)
Default country
When initialCountryCode is omitted, the picker starts with Jordan:
| Field | Value |
|---|---|
| Name | Jordan |
| ISO | JO |
| Dial code | +962 |
| Hint | 7X-XXXXXXX |
| Length | 9 |
| Starts with | 77, 78, 79 |
Dependencies
flutter(SDK)country_flags^4.1.2
Requirements
- Flutter
>=1.17.0 - Dart
^3.7.2
Contributing
Pull requests are welcome.
License
See LICENSE.
Changelog
See CHANGELOG.md.