WingoUI
ComponentsTemplatesDocsChangelogBlogAI agents
ThemePricing
  1. Home
  2. Blog
  3. Build it: screens and apps
  4. Romanian Address Format: The Rules and a React Form
  1. Blog
  2. Build it: screens and apps
  3. Romanian Address Format: The Rules and a React Form
Build it: screens and apps
Build it: screens and apps

Romanian Address Format: The Rules and a React Form

SR
Serban Rusu · Founder of Wingo UI
Oct 9, 2026 · 10 min read

On this page

0%
  1. What is the Romanian address format?
  2. How do you write a Bucharest or village address?
  3. How do Romanian postal codes relate to counties?
  4. How do I build a Romanian address form in React?
  5. How do I search localities without diacritics?
  6. How do I load every village from SIRUTA?
  7. How do I validate the address on the server?
  8. What should I build next?
  9. FAQ
    1. How do you write an address in Romania?
    2. How many digits does a Romanian postal code have?
    3. What do bl., sc., et. and ap. mean in a Romanian address?
    4. How many counties does Romania have?
    5. Should a Romanian address form require diacritics?
    6. Are Address Input and the Romanian locations data free?

TL;DR

The Romanian address format starts with the street line (type, name, number, then bl., sc., et. and ap. for block, staircase, floor and apartment), adds the sector in Bucharest, and ends with the 6-digit postal code, the locality and, in domestic use, the county as jud. Cluj. The first two digits of the postal code identify the county, so a form can flag a code from the wrong one. In React, build it as a county select, a locality search that ignores diacritics, street and number, an optional block row and a numeric postal code.

Published Oct 9, 2026

Search for an address form React example and you get a US layout: street, city, state, ZIP. Ship that to customers in Romania and they have nowhere to put the block, the staircase or the Bucharest sector, so the courier calls to ask. This tutorial covers the Romanian address format part by part, checked against the Universal Postal Union's sheet for Romania, with English labels and Romanian data. Then it builds the form in React on Address Input, a component from Wingo UI, the library we build, so we are not neutral.

What is the Romanian address format?

A Romanian address runs from the most specific line to the broadest: the street line with the building details, the sector in Bucharest, then the postal code and the locality. The UPU addressing sheet for Romania (opens in a new tab) (dated October 2007) gives four rules:

  1. The street type comes before the name: Str. (strada), Bd. (bulevardul), Calea, Aleea, Șos. (șoseaua).
  2. The number usually ends the street line.
  3. Building details can go at the end of the same line: block, staircase, floor, apartment.
  4. In Bucharest, the sector can sit on its own line between the street and the locality.

The 6-digit postal code goes to the left of the locality. Here is a Romania address format example from the sheet for an apartment in Bucharest, and one for an office in Craiova:

text
ASTRA
Drumul Taberei nr. 35, bl. F5, sc. 2, parter, ap. 23
Sector 6
061357 BUCHAREST
ROMANIA
SC PASAJ SA
Str. Bariera Valcii, nr. 215
200716 CRAIOVA
ROMANIA

The sheet writes Valcii for Vâlcii, which matters for search later. Both stop at the locality, since the postal code implies the county. In domestic mail, invoices and courier forms, people add the county (județ) after the locality as jud. Cluj, which also tells apart villages that share a name. Here is the Romanian postal address format as form fields, with English labels:

Form labelRomanianWritten as
Countyjudețjud. Cluj
Sectorsector (București only)Sector 6
City or villagemunicipiu, oraș, comună, satCluj-Napoca, sat Vâlcele
Streetstradă, bulevard, calea, aleeaStr. Memorandumului
No.numărnr. 28
Blockblocbl. A2
Entrancescarăsc. 1
Flooretajet. 3, or parter for the ground floor
Apt.apartamentap. 12
Postal codecod poștal400114

The labels are Address Input's English defaults. We label the scară "Entrance" because each staircase of a block has its own door, which is what a courier looks for.

How do you write a Bucharest or village address?

In Bucharest the sector takes the place of the county, and in the countryside a village usually needs its commune as well. București is a municipality with the rank of a county, split into six sectors. Written on one line, as a courier form or an invoice prints it, a Bucharest address ends with "Sector 6, 061357 București" and has no jud. part.

Villages add a level. Counties contain municipalities (municipii), towns (orașe) and communes (comune), and a commune groups several villages (sate). The National Institute of Statistics keeps the official list as SIRUTA, a register on those three levels that it publishes as a CSV on data.gov.ro (opens in a new tab). A village address names the village and usually its commune, as in "sat Vâlcele, com. Feleacu, jud. Cluj": Feleacu (opens in a new tab) is a commune of five villages, one of them Vâlcele. In many rural communities (opens in a new tab) there are no street names at all.

RoAddress, the value type, has no commune field. Keep the village in locality and its SIRUTA code in localityId: the register links every village to its commune, so you can print the commune when a courier needs it.

How do Romanian postal codes relate to counties?

The first two digits of a Romanian postal code identify the county, or the sector in Bucharest. Romania moved to 6-digit codes on May 1, 2003, and the UPU sheet reads them as a postal region, a district of that region (or a sector in Bucharest), then the delivery area. Per Wikipedia's article on postal codes in Romania (opens in a new tab), cities of more than 50,000 people get a code per street or small group of addresses, a smaller town has one code for the whole town, and village codes have a 7 as their third digit, like Feleacu's 407270. Poșta Română's postal code search (opens in a new tab) finds the full code for an address.

Here is the Romanian counties list with the ISO 3166-2:RO (opens in a new tab) code (the letters on license plates) and the postal prefix. Address Input stores the county as this code, and the prefixes match its RO_POSTAL_PREFIXES:

CountyCodePostal prefix
AlbaAB51
AradAR31
ArgeșAG11
BacăuBC60
BihorBH41
Bistrița-NăsăudBN42
BotoșaniBT71
BrașovBV50
BrăilaBR81
BucureștiB01 to 06 (Sector 1 to 6)
BuzăuBZ12
Caraș-SeverinCS32
CălărașiCL91
ClujCJ40
ConstanțaCT90
CovasnaCV52
DâmbovițaDB13
DoljDJ20
GalațiGL80
GiurgiuGR08
GorjGJ21
HarghitaHR53
HunedoaraHD33
IalomițaIL92
IașiIS70
IlfovIF07
MaramureșMM43
MehedințiMH22
MureșMS54
NeamțNT61
OltOT23
PrahovaPH10
Satu MareSM44
SălajSJ45
SibiuSB55
SuceavaSV72
TeleormanTR14
TimișTM30
TulceaTL82
VasluiVS73
VâlceaVL24
VranceaVN62

That gives a cheap check. If someone picks Iași and types a code that starts with 40, one of the two is wrong: postalCodeMatchesCounty("400114", "IS") returns false. In Bucharest a chosen sector narrows it to one prefix, so postalCodeMatchesCounty("061357", "B", "6") is true and the same code with sector "3" is false.

Address Input shows a mismatch as an amber warning under the postal code once the field loses focus, and the form stays valid. We chose a warning on purpose: the check only knows prefixes, and a person who knows their own code should never be stuck at checkout.

How do I build a Romanian address form in React?

Split it into a county select, a locality search, street and number, an optional block row and a six-digit postal code, then validate on submit and again on the server. Address Input ships those parts. It is a Pro component, so sign in once with npx wingo-ui@latest login on a Pro account, then run npx wingo-ui@latest add address-input. The CLI adds what it builds on, including ro-locations, text-match, the select and the combobox. The component renders the county, the locality, street and number, a collapsed row for block, staircase, floor and apartment, the postal code and a details field for the intercom or a landmark. Its labels follow the locale (the locale prop, then the provider, then the page language): English, or Romanian for ro.

A checkout step that validates on submit and saves through the Server Action shown further down:

tsx
// app/checkout/shipping-step.tsx
"use client";
import { useState } from "react";
import {
AddressInput,
EMPTY_RO_ADDRESS,
validateRoAddress,
type AddressErrors,
type RoAddress,
} from "@/components/ui/address-input";
import { Button } from "@/components/ui/button";
import { saveShipping } from "./actions";
export function ShippingStep({ onDone }: { onDone: () => void }) {
const [address, setAddress] = useState<RoAddress>(EMPTY_RO_ADDRESS);
const [errors, setErrors] = useState<AddressErrors>();
return (
<form
noValidate
className="flex flex-col gap-4"
onSubmit={async (event) => {
event.preventDefault();
// the submit already made AddressInput show every error
if (!validateRoAddress(address).valid) return;
const result = await saveShipping(address);
if (result.ok) onDone();
else setErrors(result.errors);
}}
>
<AddressInput
label="Delivery address"
value={address}
errors={errors}
onValueChange={(next) => {
setAddress(next);
setErrors(undefined);
}}
showSummary
/>
<Button type="submit">Continue to payment</Button>
</form>
);
}

showSummary prints formatRoAddress under the form, the way the courier will read it: Str. Memorandumului 28, bl. A2, sc. 1, et. 3, ap. 12, 400114 Cluj-Napoca, jud. Cluj.

The parts a hand-rolled form usually misses:

  • Phones. The county and locality lists open as bottom sheets below 768px, the postal code takes six digits with the numeric keypad, and Enter moves to the next part.
  • Autofill. The county uses address-level1, the locality address-level2, the street address-line1 and the code postal-code, so a saved browser address fills the parts.
  • Paste. "Str. Memorandumului nr. 28, bl. A2" pasted into the street fills the street, number and block.
  • County changes. Picking București swaps the locality for a sector select. Changing the county clears the locality and a postal code from another county, and keeps the street and number.
Clear the postal code, type six digits that start with 70, the prefix of Iași, and press Tab: the county is Cluj, so a warning appears while the readout still says valid. Then pick București as the county to get the sector select
$ npx wingo-ui@latest add address-input
ProAddress Input docs

How do I search localities without diacritics?

Fold both strings before you compare them: lowercase, decompose with NFD and drop the combining marks. "Iași".includes("iasi") is false, and folded, both sides read iasi. Ș and ț with a comma below only arrived in Unicode 3.0 in 1999, and older encodings had only the cedilla forms ş and ţ (opens in a new tab), which still turn up in imported data. Both decompose to a base letter plus a combining mark, so NFD folding treats Ștefănești, Ştefăneşti and stefanesti as one name.

text-match does that folding, ranks matches (exact, prefix, word prefix, inside a word) and returns the ranges to highlight, in about 200 lines with no dependencies. ro-locations builds its searchLocations on it, and Address Input's locality combobox filters with it.

Type iasi, manastur or sector 3 without diacritics: each finds its place, with the matched letters highlighted
$ npx wingo-ui@latest add ro-locations
ProRomanian locations docs

Folding does not cover abbreviations or old spellings. People type "Tg. Mureș" for Târgu Mureș, and records from before the 1993 spelling rule write î inside words (Tîrgu). filterByText takes keywords next to the label, so aliases handle both:

ts
// lib/locality-search.ts
import { filterByText } from "@/lib/text-match";
// county is the plate code ("CJ"), the code Address Input stores and passes to loadLocalities
export type Locality = { id: string; name: string; county: string; commune?: string };
// how people also write a name: "Tg. Mures", and "Tirgu" from before the 1993 spelling rule
function aliases(name: string) {
return [name.replace(/^Târgu /, "Tg. "), name.replace(/(?<=\p{L})â(?=\p{L})/gu, "î")];
}
export function searchLocalities(list: readonly Locality[], county: string, query: string) {
return filterByText(
list.filter((locality) => locality.county === county),
query,
(locality) => [locality.name, ...aliases(locality.name)],
{ limit: 20 },
);
}

Now "tg mures" and "tirgu mures" both find Târgu Mureș, ranked a little below a direct hit on the name.

How do I load every village from SIRUTA?

Serve the SIRUTA list from your own route and give Address Input a loadLocalities function that queries it. Without one, the component searches a built-in list of 332 cities, towns and large communes and accepts any other locality as typed, with a quiet note under the field. text-match has no "use client" directive, so the same search runs in a route handler:

ts
// app/api/localities/route.ts
import type { NextRequest } from "next/server";
import { LOCALITIES } from "@/data/localities"; // built from the SIRUTA CSV
import { searchLocalities } from "@/lib/locality-search";
export function GET(request: NextRequest) {
const params = request.nextUrl.searchParams;
const results = searchLocalities(LOCALITIES, params.get("county") ?? "", params.get("q") ?? "");
return Response.json(
results.map(({ item }) => ({
value: item.id,
label: item.name,
// tells apart villages with the same name
description: item.commune && `com. ${item.commune}`,
})),
);
}
tsx
<AddressInput
label="Delivery address"
loadLocalities={async (county, query, { signal }) => {
const params = new URLSearchParams({ county, q: query });
const response = await fetch(`/api/localities?${params}`, { signal });
return response.ok ? response.json() : [];
}}
/>

The value you return becomes localityId, so the SIRUTA code is saved with the address.

How do I validate the address on the server?

Check it again in the Server Action, with a zod 4 schema (npm install zod) in a file that has no "use client". Copied component code has a trap here: address-input.tsx starts with "use client", so on the server every export is a client reference. Calling validateRoAddress or formatRoAddress in a Server Component or a Server Action throws "Attempted to call formatRoAddress() from the server but formatRoAddress is on the client". Type imports are fine, and ro-locations has no directive, so the schema can use its county lookup:

ts
// lib/ro-address-schema.ts
import { z } from "zod";
import { getCounty } from "@/lib/ro-locations";
const text = z.string().trim();
export const roAddressSchema = z
.object({
county: text.refine((code) => Boolean(getCounty(code)), { error: "Choose a county from the list" }),
locality: text,
localityId: text.optional(),
sector: z.enum(["1", "2", "3", "4", "5", "6"]).optional(),
street: text.min(1, { error: "Enter the street" }),
number: text.min(1, { error: "Enter the number" }),
building: text.optional(),
entrance: text.optional(),
floor: text.optional(),
apartment: text.optional(),
postalCode: z.union([z.literal(""), z.string().regex(/^\d{6}$/, { error: "A postal code has 6 digits" })]).optional(),
details: text.optional(),
})
// bucharest has sectors where other counties have localities
.refine((a) => a.county.toUpperCase() !== "B" || Boolean(a.sector), { error: "Choose a sector", path: ["sector"] })
.refine((a) => a.county.toUpperCase() === "B" || a.locality.length > 0, {
error: "Enter the city or village",
path: ["locality"],
});
ts
// app/checkout/actions.ts
"use server";
import type { AddressErrors } from "@/components/ui/address-input";
import { roAddressSchema } from "@/lib/ro-address-schema";
export async function saveShipping(input: unknown): Promise<{ ok: true } | { ok: false; errors: AddressErrors }> {
const parsed = roAddressSchema.safeParse(input);
if (!parsed.success) {
// the first message per field, the shape of AddressInput's errors prop
const errors: AddressErrors = {};
for (const issue of parsed.error.issues) errors[issue.path[0] as keyof AddressErrors] ??= issue.message;
return { ok: false, errors };
}
// save parsed.data with the order
return { ok: true };
}

Errors from the action land on the matching part through the errors prop and win over the component's own. To format an address on the server for an order email, move formatRoAddress and its helpers into a module without the directive. You own the source.

What should I build next?

Try the postal code check in the demo above, then add the form to your checkout with npx wingo-ui@latest add address-input. Address Input and ro-locations are part of Wingo UI Pro ($8 a month, $80 a year or $150 once as of October 2026), and text-match is free. If the same checkout takes company details, Romanian CUI validation covers tax IDs and the IBAN validation guide covers bank accounts, while React form components explains the label, hint and error anatomy every part of this form shares. More step-by-step React builds live in the Build it category.

Components in this post

  • Address Input

    A Romanian address form: county, locality search, street, number, block and apartment, postal code checked against the county.

    Pro
  • Romanian locations

    Romania's counties, cities and neighbourhoods with coordinates, diacritic-free search, distances and within-radius lists.

    Pro
  • Text match

    Search that ignores case and diacritics, ranks the best matches first and returns the ranges to highlight.

    Free

FAQ

How do you write an address in Romania?

Write the street type, name and number first (Str. Memorandumului 28), then the block, staircase, floor and apartment on the same line (bl. A2, sc. 1, et. 3, ap. 12). The next line holds the 6-digit postal code and the locality (400114 Cluj-Napoca), followed in domestic use by the county as jud. Cluj, and in Bucharest a Sector line goes above the postal code. Mail from abroad ends with ROMANIA on its own line.

How many digits does a Romanian postal code have?

Six. Six-digit codes replaced the four-digit ones on May 1, 2003. The first two digits identify the county, and in Bucharest they identify the sector, from 01 for Sector 1 to 06 for Sector 6.

What do bl., sc., et. and ap. mean in a Romanian address?

They abbreviate bloc (the apartment building), scară (the staircase, which has its own entrance), etaj (the floor) and apartament (the apartment). The ground floor is written parter.

How many counties does Romania have?

Romania has 41 counties (județe) plus the municipality of Bucharest, which is divided into six sectors. Their ISO 3166-2:RO codes, such as RO-CJ for Cluj and RO-B for Bucharest, use the same letters as Romanian license plates.

Should a Romanian address form require diacritics?

No. Accept what people type and match it without diacritics: the UPU's own sample addresses for Romania are written without them, and older data uses the cedilla forms ş and ţ. Store the official spelling from your locality list once the person picks a match.

Are Address Input and the Romanian locations data free?

Address Input and ro-locations are part of Wingo UI Pro. text-match, the diacritic-insensitive search both of them use, is free and installs with npx wingo-ui@latest add text-match.

  • Romania
  • Address Form
  • React
  • Postal Codes
  • Forms
  • Next.js

Share

SR

About the author

Serban Rusu

Founder of Wingo UI

Serban Rusu is the founder of Wingo UI. He builds the component library, its CLI and its MCP server, and writes about React interfaces that work well on phones and with AI coding agents.

More from Serban
Keep reading

Related posts

All posts
Build it: screens and apps

E-Factura VAT Calculation in TypeScript and React

E-Factura VAT calculation in TypeScript: Romania's 21% and 11% rates, VAT rounded once per rate, the totals ANAF checks to the cent and a React line editor.

SRSerban Rusu·Oct 9, 2026·11 min read
Build it: screens and apps

Nextjs Admin Dashboard Tutorial: 5 Screens, Phone-Ready

A nextjs admin dashboard tutorial for the App Router: app shell, KPI home, server-side data tables, settings and team pages, each one usable on a phone.

SRSerban Rusu·Oct 9, 2026·19 min read
Mobile UI in React

100vh Mobile Fix: When to Use dvh, svh or lvh in Tailwind

The 100vh mobile bug explained: why h-screen hides your bottom bar, when to use dvh, svh or lvh in Tailwind, and how to keep a bar above the keyboard.

SRSerban Rusu·Oct 9, 2026·11 min read
Newsletter

Get new posts by email

New guides, tutorials and comparisons from the Wingo UI blog, sent when they are published.

No spam. Unsubscribe at any time.

WingoUI

Animated, configurable, mobile-first React components. Copy the source, make it yours, and let your coding agent build with it.

ComponentsTemplatesPricingBlogTheme

Component categories

  • Buttons & Actions
  • Inputs
  • Forms
  • Navigation
  • Overlays
  • Feedback
  • Data Display
  • Tables & Lists
  • Charts & Stats
  • Layout
  • Media
  • AI Kit
  • Text & Effects
  • Mobile
  • Commerce
  • Marketing Sections
  • Blocks
  • Hooks & Utilities
Wingo UI