pakistan-address-react
v0.4.0
Published
Cascading Province, Division, District and Tehsil address fields for React, with an "Other / not listed" option at every level. Built on pakistan-address.
Maintainers
Readme
pakistan-address-react
Cascading Province → Division → District → Tehsil address fields for React, built on pakistan-address.
- Each level filters from the previous one.
- "Other / not listed" at every level below province, revealing a free-text input.
- Islamabad (no divisions) and areas with no tehsil data fall back to the right inputs automatically.
- A separate full address / landmark field.
- Searchable dropdowns. Long lists (more than 10 options, such as districts and delivery cities) get a search box: type to filter, arrow keys to move, Enter to choose. Alternate names match too ("Nawabshah" finds Shaheed Benazir Abad). Short lists stay a plain dropdown.
- Custom, accessible dropdowns (WAI-ARIA combobox), no extra dependencies, styled through class names. A native
<select>is one prop away (native). Or use the headless hook with any select library.
Data baseline is the 2017 census; see the data notes for what is and isn't covered.
Install
npm install pakistan-address-react pakistan-addressRequires React 18+.
Ready-made fields
import { AddressFields, type ResolvedAddress } from 'pakistan-address-react';
function Checkout() {
const [address, setAddress] = useState<ResolvedAddress>();
return (
<form>
<AddressFields name="shipping" onChange={(_value, resolved) => setAddress(resolved)} />
</form>
);
}resolved looks like:
{
"province": "Punjab", "division": "Multan", "district": "Vehari", "tehsil": "Mailsi",
"addressLine": "House 5, near Jamia Masjid",
"ids": { "province": "pb", "division": "pb-multan-div", "district": "pb-vehari", "tehsil": "pb-vehari-mailsi" }
}Where the user typed their own text, the name is that text and the matching ids entry is null.
Props: value / defaultValue / onChange (controlled or uncontrolled), labels (translate or rename), classNames, name (prefix, so the fields post with a native form: shipping[provinceId], shipping[districtOther], shipping[addressLine], …), native, searchThreshold and unstyled.
The dropdowns
<AddressFields
searchThreshold={5} // search box for lists longer than 5 options (default 10)
native // or: use the browser's own <select> (no search box)
unstyled // drop the built-in inline styles and style everything yourself
labels={{ searchPlaceholder: 'Type to search…', noResults: 'Nothing found', placeholder: 'Choose…' }}
classNames={{ select: 'input', listbox: 'menu', option: 'menu-item', clear: 'clear-btn' }}
/>classNameskeys:root,field,label,select(the dropdown's text box, or the<select>whennative),input(typed-in text and the address line),notice, and for the custom dropdowncombobox,control,clear,toggle,listbox,option,empty. The active and selected options carrydata-active/aria-selectedfor CSS ([data-active] { … }), and the list setsdata-openon the root.- Without
unstyledthe dropdown uses a few inline styles so it works with no CSS (system colours, positioned list). - The chosen id is posted by a hidden input, so native forms and
FormDataseename[provinceId]as before. With JavaScript off the custom dropdown can't open; usenativeif you need a no-JS form. - The dropdown is exported on its own as
Combobox(withfilterOptions) if you want the same control elsewhere:optionsof{ value, label, keywords?, pinned? },value,onChange.
Headless hook (react-select, MUI, Radix…)
import Select from 'react-select';
import { OTHER, useAddressCascade } from 'pakistan-address-react';
function ProvinceDivisionDistrict() {
const c = useAddressCascade();
const withOther = (level: 'division' | 'district') => [
...c.levels[level].options,
{ value: OTHER, label: 'Other / not listed' },
];
const find = (options: { value: string }[], id: string | null) => options.find((o) => o.value === id) ?? null;
return (
<>
<Select
aria-label="Province"
options={c.levels.province.options}
value={find(c.levels.province.options, c.value.province.id)}
onChange={(o) => c.select('province', o?.value ?? null)}
/>
{c.levels.division.visible && (
<Select
aria-label="Division"
options={withOther('division')}
value={find(withOther('division'), c.value.division.id)}
onChange={(o) => c.select('division', o?.value ?? null)}
/>
)}
{c.levels.district.visible && (
<Select
aria-label="District"
options={withOther('district')}
value={find(withOther('district'), c.value.district.id)}
onChange={(o) => c.select('district', o?.value ?? null)}
/>
)}
{c.levels.district.showText && (
<input
aria-label="District (other)"
value={c.value.district.text}
onChange={(e) => c.setText('district', e.target.value)}
/>
)}
</>
);
}Levels appear one at a time (division, then district, then tehsil), so render each one only when c.levels[level].visible is true. c.levels[level] gives { visible, options, allowOther, showText } for each level, c.address the resolved value, and c.select, c.setText, c.setAddressLine, c.reset update it. The same logic is available without React as getLevelStates, selectLevel, setLevelText and resolveAddress.
Unofficial delivery areas, and switching between the two lists
Besides the official hierarchy, pakistan-address has an optional, unofficial list of courier delivery areas (province, city, zone) taken from an online store's address form; see its README. The fields for it live in a separate entry point, so the main bundle does not include that data:
import { DeliveryAddressFields, SwitchableAddressFields } from 'pakistan-address-react/delivery';
// Delivery areas only (shows a note that the list is unofficial)
<DeliveryAddressFields name="shipping" onChange={(_value, resolved) => setAddress(resolved)} />
// A switch between "Official administrative units" and "Delivery areas (unofficial)"
<SwitchableAddressFields
defaultSource="official"
onChange={(address) => setAddress(address)} // address.source is 'official' or 'delivery'
/>DeliveryAddressFieldstakes the same props asAddressFields(value,defaultValue,onChange,labels,classNames,name) plushideNotice. Itsresolvedvalue is{ province, city, area, zone, addressLine, ids }withdl-...ids (areais empty for cities that have no areas, and the Area field is then not shown). There is also a headlessuseDeliveryCascade.SwitchableAddressFieldstakessource/defaultSource/onSourceChange,hideSwitch,switchLabels, andofficialProps/deliveryProps. Switching clears the fields, andonChangetags each value with itssource. Importing it includes both datasets (the delivery one is about 1.2 MB unpacked).- The delivery note is on by default. Only turn it off if you tell your users some other way that the list is unofficial.
Behaviour notes
- Changing a level clears every level below it.
- Choosing "Other" for a division shows all districts of the province; "Other" for a district makes tehsil a text input.
- Tehsil is shown as plain text input whenever the district has no tehsil data.
