Address Input React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React address input for Romanian addresses: county, locality search, street, number, block and apartment, and a checked postal code.
Pick the county from a searchable list (type to filter, arrows and Enter; a bottom sheet on phones), then type the locality: the cities of that county show as you type, diacritics do not matter ("cluj", "iasi"), Enter picks the highlighted one, and a village that is not in the list is kept as typed through the "Use …" row, with a quiet note under the field (never an error) that it is not in the list and to check the spelling. Picking a city before the county fills the county too. Choosing București swaps the locality for a Sector select; changing the county clears the locality and a postal code from another county but keeps the street and the number. Street and number are side by side from 512px of width; "Add block, entrance, apartment" opens the bl. / sc. / et. / ap. row on a smooth collapse and moves focus into it (it is open from the start when the value has any of them). The postal code takes digits only, six at most, with the numeric keypad on phones; leaving it with a code from another county shows an amber warning with an icon under it, never an error. Enter in a part moves to the next one and submits only from the last; the phone keyboard says Next and then Done. Required parts show their message on blur (or on change, or only after the form is submitted); server errors land on their field, and a county code the list does not know (old data such as "BUC") is flagged at once. Browser autofill works on every part, and "Str. Memorandumului nr. 28, bl. A2" pasted or autofilled into the street is split into the parts; a full address ("…, sat Feleacu, jud. Cluj, 407280") also fills the county, locality, sector and postal code, but only the ones that are still empty, and never a locality from another county than the chosen one. The single-line variant is one search field: type an address, the suggestions (offline parsing and the localities list, or your searchAddress) fill every part and open the details below for a final check; leaving the field with text that was not picked (Tab or a tap outside) parses that text into the parts. Read-only shows the formatted address with a copy button; reduced motion turns the collapses into fades.
Address Input is a Pro React component in the React form components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add address-input, together with Button, Collapsible, Combobox, Field, Input and 7 more and the npm packages motion and lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add address-inputAddress Input is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import AddressInput from @/components/ui/address-input.
import { useState } from "react"
import { AddressInput, EMPTY_RO_ADDRESS, type RoAddress } from "@/components/ui/address-input"
export function ShippingAddress() {
const [address, setAddress] = useState<RoAddress>(EMPTY_RO_ADDRESS)
const [valid, setValid] = useState(false)
return (
<form action="/api/checkout" className="flex flex-col gap-4">
<AddressInput
label="Delivery address"
name="shipping"
value={address}
onValueChange={(next, validation) => {
setAddress(next)
setValid(validation.valid)
}}
showSummary
/>
<button type="submit" disabled={!valid}>Continue</button>
</form>
)
}Props
valueRoAddressThe controlled address: { county (plate code "CJ", "B"), locality, localityId?, sector? (București), street, number, building?, entrance?, floor?, apartment?, postalCode?, details? }.
defaultValuedefault"EMPTY_RO_ADDRESS"RoAddressThe starting address when uncontrolled.
onValueChange(address: RoAddress, validation: { valid: boolean; errors: AddressErrors; warnings: AddressErrors }) => voidEvery change with the whole address and its validity, so a submit button can follow valid.
variantdefault"form""form" | "single"form: the parts. single: one autocomplete field that fills the parts, with the parts under an "Edit details" collapsible.
layoutdefault"auto""auto" | "stacked"auto: one column until the component is 512px wide, then 6 columns (county and locality 3 + 3, street 4 and number 2, postal code 2 and details 4). stacked: one column always.
fieldsdefault"all"AddressField[]Which parts render: a courier form hides details, a billing form hides the apartment row.
requiredFieldsdefault"["county", "locality", "street", "number"]"AddressField[]Marked with * and validated. In București the sector is required in place of the locality.
collapseApartmentdefaulttruebooleanThe block / entrance / floor / apartment row waits behind a link until used; it is open when the value has any of them.
validateOndefault"blur""blur" | "change" | "submit"When a part's error shows. A submit of the enclosing form shows them all.
errorsAddressErrorsServer errors per field, e.g. { street: "This street is not in the official street list" }. They win over the computed ones.
showSummarydefaultfalsebooleanThe formatted one-line address under the form, crossfading as it changes.
countryLabeldefault""ReactNodeA read-only country row at the top ("Romania"), for forms that print the country.
loadLocalities(county: string, query: string, context: { signal: AbortSignal }) => Promise<{ value: string; label: string; description?: string }[]>Your full nomenclature (SIRUTA). value becomes localityId. Without it the cities of the county come from ro-locations plus a built-in list of every Romanian town (Pașcani, Onești, Moinești...), and free text is accepted for villages.
searchAddress(query: string, context: { signal: AbortSignal }) => Promise<AddressSuggestion[]>The single variant's online suggestions ({ id, label, description?, address }), debounced 400ms. Without it the text is parsed offline.
labeldefault""ReactNodeThe group's legend (a fieldset); every part has its own label too. The demo shows "Delivery address" while it is empty.
descriptiondefault""ReactNodeHelper text under the legend.
errordefault""ReactNodeA message for the whole group, linked to the fieldset.
requireddefaultfalsebooleanThe * after the legend. Which parts are required is requiredFields.
invaliddefaultfalsebooleanEvery part in the invalid look without a message.
disableddefaultfalsebooleanDims and blocks every part (a disabled fieldset).
readOnlydefaultfalsebooleanThe formatted address as text on several lines with a copy button; "-" when empty.
sizedefault"md""sm" | "md" | "lg"Every part 32 / 40 / 48px (md 44px on touch), with matching label and text sizes.
fieldVariantdefault"soft""soft" | "outline" | "underline" | "ghost"The box of every part (variant is the form / single choice here).
radiusdefault"md""none" | "sm" | "md" | "lg" | "full"Corners of every box, scaled per size like the button's.
namedefault""stringAdds hidden inputs and names for native forms: `${name}.county`, `${name}.locality`, `${name}.localityId`, `${name}.street`… The demo lists them in the readout.
responsivedefaulttruebooleanThe county, sector and locality lists open as bottom sheets below 768px (the locality sheet has its own search box).
localedefault""stringA ro locale picks ADDRESS_INPUT_LABELS_RO on its own. Empty uses the UIProvider locale, then the browser's. The demo defaults to en-US.
labelsPartial<AddressInputLabels>Every string: the part labels (county, locality, sector, street, number, building, entrance, floor, apartment, postalCode, details, country), more, search, editDetails, required, postalInvalid, postalMismatch(county), localityCustom(text), localityUnlisted(text), noLocalities, placeholders, countyUnknown, noResults, loading, copy, copied, filled, clear, cancel, showSuggestions. English by default; ADDRESS_INPUT_LABELS_RO is the ready Romanian set.
fieldPropsPartial<FieldGroupProps>More for the fieldset: variant "card", classNames.
idstringThe prefix of every part's id (`${id}-street`).
classNamesPartial<Record<"grid" | "country" | "county" | "locality" | "sector" | "street" | "number" | "more" | "apartmentRow" | "postal" | "details" | "warning" | "single" | "editDetails" | "summary" | "readOnly", string>>Extra classes for the inner parts.
classNamestringExtra classes for the root inside the fieldset.
Parts
formatRoAddress
function"Str. Memorandumului 28, bl. A2, sc. 1, et. 3, ap. 12, 400114 Cluj-Napoca, jud. Cluj"; București ends with "Sector 3, 030167 București". Signature: (address, { multiline = false, includeCounty = true, includeCountry = false }) => string.
validateRoAddress
functionThe component's validation, for a form's own submit handler. A postal code from another county is a warning. Signature: (address, { requiredFields?, fields?, labels? }) => { valid; errors; warnings }.
parseRoAddress
functionBest effort: street type and name, nr. or a trailing number, bl. / sc. / et. / ap., a 6-digit postal code, sector, a known locality and county. Signature: (text: string) => Partial<RoAddress>.
postalCodeMatchesCounty
functionfalse only for a complete code whose first two digits belong to another county (RO_POSTAL_PREFIXES). Signature: (code: string, county: string, sector?: string) => boolean.
RO_POSTAL_PREFIXES
constantThe two-digit postal code prefixes per county (București 01 to 06), used by postalCodeMatchesCounty. Type: Record<string, string[]>.
RO_STREET_TYPES
constantThe street types the street field offers and the parser recognises ("Str.", "Bd.", "Calea"…). Type: readonly string[].
EMPTY_RO_ADDRESS
constantThe empty address, a starting value for a controlled form. Type: RoAddress.
DEFAULT_ADDRESS_INPUT_LABELS
constantThe English default labels. Type: AddressInputLabels.
ADDRESS_INPUT_LABELS_RO
constantThe ready Romanian label set (Județ, Localitate, Bloc, Scara, Etaj, Ap.). Type: AddressInputLabels.
RoAddress
typeThe value: county (the plate code, "B" for Bucharest), locality, localityId, sector (Bucharest only), street, number, building, entrance, floor, apartment, postalCode and details.
AddressField
typeA key of RoAddress, used by fields, requiredFields and errors.
AddressErrors
typeMessages per part, Partial<Record<AddressField, string>>, for server errors through errors.
AddressSuggestion
typeOne hit of searchAddress: id, label, description and a partial RoAddress that fills the parts.
Examples
București
Choosing București swaps the locality for a sector select; the summary ends in "Sector 3, 030167 București".
A village not in the list
Localities outside the offline list are typed freely; 407280 matches Cluj, so no warning.
Postal code warning
400114 is a Cluj code in county Iași: a warning with an icon, never a blocking error.
Single-line variant
One field with offline parsing; a suggestion fills the parts and opens them for bl. / ap.
Billing address
No apartment row and no details, a read-only country row and outline boxes.
Validation on submit
Errors wait for the submit; the server's answer lands on the street field.
Read-only
The formatted address as text with a copy button, for order details.
Dependencies
Files
- components/ui/address-input.tsx
FAQ
Is the React Address Input component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Address Input?
Run npx wingo-ui@latest add address-input. It also adds the Wingo UI items it builds on (Button, Collapsible, Combobox, Field, Input and 7 more) and the npm packages motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/address-input.
How does Address Input behave on phones?
The county, sector and locality lists open as bottom sheets below 768px (the locality sheet has its own search box). The responsive prop controls this and is on by default.
See also
Used in
More React form components
Articles using this component
- Romanian Address Format: The Rules and a React Form
Build it: screens and apps10 min read