# Address Input

> A React address input for Romanian addresses: county, locality search, street, number, block and apartment, and a checked postal code.

- Type: React component (Forms)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add address-input`
- Docs: https://wingo-ui.com/components/address-input

## Interaction

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.

## Installation

```bash
npx wingo-ui@latest add address-input
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/address-input` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/address-input.tsx`

npm dependencies: `npm i motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Collapsible](https://wingo-ui.com/components/collapsible) ([Markdown](https://wingo-ui.com/components/collapsible.md))
- [Combobox](https://wingo-ui.com/components/combobox) ([Markdown](https://wingo-ui.com/components/combobox.md))
- [Field](https://wingo-ui.com/components/field) ([Markdown](https://wingo-ui.com/components/field.md))
- [Input](https://wingo-ui.com/components/input) ([Markdown](https://wingo-ui.com/components/input.md))
- [Select](https://wingo-ui.com/components/select) ([Markdown](https://wingo-ui.com/components/select.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useLocale](https://wingo-ui.com/components/use-locale) ([Markdown](https://wingo-ui.com/components/use-locale.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Romanian locations](https://wingo-ui.com/components/ro-locations) ([Markdown](https://wingo-ui.com/components/ro-locations.md))
- [Text match](https://wingo-ui.com/components/text-match) ([Markdown](https://wingo-ui.com/components/text-match.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
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

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value?` | `RoAddress` |  | The controlled address: { county (plate code "CJ", "B"), locality, localityId?, sector? (București), street, number, building?, entrance?, floor?, apartment?, postalCode?, details? }. |
| `defaultValue?` | `RoAddress` | `"EMPTY_RO_ADDRESS"` | The starting address when uncontrolled. |
| `onValueChange?` | `(address: RoAddress, validation: { valid: boolean; errors: AddressErrors; warnings: AddressErrors }) => void` |  | Every change with the whole address and its validity, so a submit button can follow valid. |
| `variant?` | `"form" \| "single"` | `"form"` | form: the parts. single: one autocomplete field that fills the parts, with the parts under an "Edit details" collapsible. |
| `layout?` | `"auto" \| "stacked"` | `"auto"` | 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. |
| `fields?` | `AddressField[]` | `"all"` | Which parts render: a courier form hides details, a billing form hides the apartment row. |
| `requiredFields?` | `AddressField[]` | `"[\"county\", \"locality\", \"street\", \"number\"]"` | Marked with * and validated. In București the sector is required in place of the locality. |
| `collapseApartment?` | `boolean` | `true` | The block / entrance / floor / apartment row waits behind a link until used; it is open when the value has any of them. |
| `validateOn?` | `"blur" \| "change" \| "submit"` | `"blur"` | When a part's error shows. A submit of the enclosing form shows them all. |
| `errors?` | `AddressErrors` |  | Server errors per field, e.g. { street: "This street is not in the official street list" }. They win over the computed ones. |
| `showSummary?` | `boolean` | `false` | The formatted one-line address under the form, crossfading as it changes. |
| `countryLabel?` | `ReactNode` | `""` | A 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. |
| `label?` | `ReactNode` | `""` | The group's legend (a fieldset); every part has its own label too. The demo shows "Delivery address" while it is empty. |
| `description?` | `ReactNode` | `""` | Helper text under the legend. |
| `error?` | `ReactNode` | `""` | A message for the whole group, linked to the fieldset. |
| `required?` | `boolean` | `false` | The * after the legend. Which parts are required is requiredFields. |
| `invalid?` | `boolean` | `false` | Every part in the invalid look without a message. |
| `disabled?` | `boolean` | `false` | Dims and blocks every part (a disabled fieldset). |
| `readOnly?` | `boolean` | `false` | The formatted address as text on several lines with a copy button; "-" when empty. |
| `size?` | `"sm" \| "md" \| "lg"` | `"md"` | Every part 32 / 40 / 48px (md 44px on touch), with matching label and text sizes. |
| `fieldVariant?` | `"soft" \| "outline" \| "underline" \| "ghost"` | `"soft"` | The box of every part (variant is the form / single choice here). |
| `radius?` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"md"` | Corners of every box, scaled per size like the button's. |
| `name?` | `string` | `""` | Adds hidden inputs and names for native forms: `${name}.county`, `${name}.locality`, `${name}.localityId`, `${name}.street`… The demo lists them in the readout. |
| `responsive?` | `boolean` | `true` | The county, sector and locality lists open as bottom sheets below 768px (the locality sheet has its own search box). |
| `locale?` | `string` | `""` | A 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. |
| `labels?` | `Partial<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. |
| `fieldProps?` | `Partial<FieldGroupProps>` |  | More for the fieldset: variant "card", classNames. |
| `id?` | `string` |  | The prefix of every part's id (`${id}-street`). |
| `classNames?` | `Partial<Record<"grid" \| "country" \| "county" \| "locality" \| "sector" \| "street" \| "number" \| "more" \| "apartmentRow" \| "postal" \| "details" \| "warning" \| "single" \| "editDetails" \| "summary" \| "readOnly", string>>` |  | Extra classes for the inner parts. |
| `className?` | `string` |  | Extra 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 (function)

The 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 (function)

Best 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 (function)

false 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 (constant)

The two-digit postal code prefixes per county (București 01 to 06), used by postalCodeMatchesCounty. Type: Record<string, string[]>.

### RO_STREET_TYPES (constant)

The street types the street field offers and the parser recognises ("Str.", "Bd.", "Calea"…). Type: readonly string[].

### EMPTY_RO_ADDRESS (constant)

The empty address, a starting value for a controlled form. Type: RoAddress.

### DEFAULT_ADDRESS_INPUT_LABELS (constant)

The English default labels. Type: AddressInputLabels.

### ADDRESS_INPUT_LABELS_RO (constant)

The ready Romanian label set (Județ, Localitate, Bloc, Scara, Etaj, Ap.). Type: AddressInputLabels.

### RoAddress (type)

The value: county (the plate code, "B" for Bucharest), locality, localityId, sector (Bucharest only), street, number, building, entrance, floor, apartment, postalCode and details.

### AddressField (type)

A key of RoAddress, used by fields, requiredFields and errors.

### AddressErrors (type)

Messages per part, Partial<Record<AddressField, string>>, for server errors through errors.

### AddressSuggestion (type)

One 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.

## See also

- [Select](https://wingo-ui.com/components/select) ([Markdown](https://wingo-ui.com/components/select.md))
- [Combobox](https://wingo-ui.com/components/combobox) ([Markdown](https://wingo-ui.com/components/combobox.md))
- [Romanian locations](https://wingo-ui.com/components/ro-locations) ([Markdown](https://wingo-ui.com/components/ro-locations.md))
- [Location Map](https://wingo-ui.com/components/location-map) ([Markdown](https://wingo-ui.com/components/location-map.md))
- [Field](https://wingo-ui.com/components/field) ([Markdown](https://wingo-ui.com/components/field.md))

---

Canonical: https://wingo-ui.com/components/address-input
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
