Address autocomplete widget in plain JavaScript

Add an address field with suggestions to any page with one script tag, and fill street, postcode and city into separate fields.

6 min Level: Beginner ✓ Code tested

We ship two ready-made scripts for an address field. They need no build tools, no framework and no other libraries. This guide shows which one to pick, how to set it up and how to get the chosen address into separate form fields.

Both scripts call /autocomplete straight from the visitor's browser. The key is therefore in your page source and must be a domain-locked browser key. Read Browser keys and allowed domains before you go live.

On a webshop checkout where you cannot write JavaScript, use the one-tag script https://api.danadresse.dk/js/danadresse-auto.min.js with data-key instead. It finds the address fields itself and fills street, postcode and city. See the platform pages under /en/integrationer.

1. Pick a script

danadresse-autocomplete-1.0.0.jsdanadresse-widget.js
URLhttps://danadresse.dk/static/js/danadresse-autocomplete-1.0.0.jshttps://danadresse.dk/static/js/danadresse-widget.js
VersionedYes, the file never changesNo, updated in place
AccessibilityARIA 1.2 combobox, live region, Home/EndARIA 1.2 combobox, live region
Drill-down (street → house number → unit)YesYes
onSelect receivesA flat object with street, house number, postcode etc.The raw suggestion from /autocomplete
Separate fieldsYou write them in onSelect (step 5)Built in: fields: { street, postnr, city }
Error displayShows a message and leaves a plain text fieldA message per cause (key, domain, too many searches, network)
Languagesda, en, ukDanish text; override with messages

Use danadresse-autocomplete-1.0.0.js for new pages. danadresse-widget.js is still served and kept up to date because existing pages use it. Use one of them per page. danadresse-widget.js always registers window.DanadresseWidget and takes window.DanadresseAutocomplete only when 1.0.0 is not on the page, so the two no longer overwrite each other.

2. Add the field and the script

A field with a label, the script and one call to attach:

HTML#basic-embed
<label for="adr">Address</label>
<input id="adr" name="address" autocomplete="off">

<script src="https://danadresse.dk/static/js/danadresse-autocomplete-1.0.0.js"></script>
<script>
  const API_KEY = "dawa_live_your_browser_key"; // domain-locked key

  DanadresseAutocomplete.attach("#adr", {
    apiKey: API_KEY,
    lang: "en",
    onSelect: function (a) {
      console.log(a.tekst, a.id);
    },
  });
</script>

Attributes alone also work. Once the page has loaded, the script finds every input with data-danadresse-key:

HTML#declarative-embed
<input id="adr" data-danadresse-key="dawa_live_your_browser_key" data-danadresse-lang="en">
<script src="https://danadresse.dk/static/js/danadresse-autocomplete-1.0.0.js"></script>

The attributes the script reads are data-danadresse-key, data-danadresse-lang, data-danadresse-type and data-danadresse-base. For an onSelect you need attach, or you listen for the danadresse:select event (see step 4).

3. Options

OptionDefaultMeaning
apiKeynoneYour browser key. Without one the API answers 401 and the field shows the error message
lang<html lang>, else dada, en or uk. Sets placeholder, empty text, error text and screen-reader text
typeadresseadresse (with floor/door), adgangsadresse (stops at the house number) or vejnavn
limit8Number of suggestions (per_side). The API allows at most 100
minLength2Characters typed before a search starts
debounce150Wait in milliseconds after the last keystroke
baseUrlhttps://api.danadresse.dkChange only if you route the calls through your own proxy
credittruefalse removes the "Addresses from danadresse.dk" line
fuzzytruefalse sends fuzzy=false and turns typo tolerance off. Default: typos are corrected
onSelect(a)noneCalled once the user has picked a final address
onError(err)noneCalled on a network or HTTP error. err.status is the status code

attach returns an object with close(), search(q) and destroy(). Calling attach twice on the same field returns the same object, so you never get duplicate lists.

4. Read the chosen address

With type: "adresse" the choice takes up to three steps. When the user picks a street name, the script inserts it, puts the caret after it and searches again. The same happens for an entrance with several flats. onSelect runs only once an address of the type you asked for has been chosen.

onSelect receives a flat object:

JSON#onselect-payload
{
  "type": "adresse",
  "tekst": "Rådhuspladsen 1, 3300 Frederiksværk",
  "id": "762a9a1f-a5fb-4e47-b1b2-9fa94026bc80",
  "adgangsadresseid": "0a3f507f-0959-32b8-e044-0003ba298018",
  "vejnavn": "Rådhuspladsen",
  "adresseringsvejnavn": "Rådhuspladsen",
  "vejkode": "1214",
  "kommunekode": "0260",
  "husnr": "1",
  "husnummer": "1",
  "husbogstav": null,
  "etage": null,
  "dør": null,
  "supplerendebynavn": null,
  "postnr": "3300",
  "postnrnavn": "Frederiksværk",
  "lat": 55.97657023,
  "lon": 12.01612365,
  "raw": { "type": "adresse", "tekst": "…", "data": { "…": "…" } }
}

husnr is the house number as one string (12A). husnummer and husbogstav are the same value split (12 and A), which many ERP and shipping systems require. raw is the original suggestion from /autocomplete, for any field the flat object does not carry.

The field dør (door) has an ø in its name. Read it with a["dør"] in JavaScript.

5. Fill separate fields

Many forms keep street, postcode and city in separate fields. Let the user search in one field and write the result into the others. Always store id: it lets you look the address up again later without parsing text.

HTML#separate-fields
<form id="delivery">
  <label for="search">Find address</label>
  <input id="search">

  <label for="street">Street and number</label>
  <input id="street" name="street">
  <label for="unit">Floor and door</label>
  <input id="unit" name="unit">
  <label for="postcode">Postcode</label>
  <input id="postcode" name="postcode" inputmode="numeric">
  <label for="city">City</label>
  <input id="city" name="city">

  <input type="hidden" id="address_id" name="address_id">
</form>

<script src="https://danadresse.dk/static/js/danadresse-autocomplete-1.0.0.js"></script>
<script>
  const API_KEY = "dawa_live_your_browser_key";
  const f = document.getElementById("delivery");

  DanadresseAutocomplete.attach("#search", {
    apiKey: API_KEY,
    lang: "en",
    onSelect: function (a) {
      f.street.value = a.vejnavn + " " + a.husnr;
      f.unit.value = [a.etage ? a.etage + "." : "", a["dør"] || ""].join(" ").trim();
      f.postcode.value = a.postnr;
      f.city.value = a.postnrnavn;
      f.address_id.value = a.id;
    },
  });

  // The user edits the fields afterwards: the id no longer matches.
  ["street", "unit", "postcode", "city"].forEach(function (name) {
    f[name].addEventListener("input", function () { f.address_id.value = ""; });
  });
</script>

If you would rather not write an onSelect, listen for the event. It bubbles up through the DOM with the same object in detail:

JavaScript#select-event
document.addEventListener("danadresse:select", (e) => {
  const a = e.detail;
  console.log("Chosen:", a.tekst, a.postnr, a.postnrnavn);
});

6. When the API does not answer

On a network error, 401, 403, 429 or 5xx the script shows "Address lookup is unavailable — type the address manually" (in the chosen language), and the field keeps working as a plain text input. Your form must therefore accept an address without an id. Cleanse it on the server with /datavask/adresser if you need the codes.

Use onError to log the cause. err.status is undefined on a network error:

JavaScript#on-error
const API_KEY = "dawa_live_your_browser_key";

DanadresseAutocomplete.attach("#adr", {
  apiKey: API_KEY,
  onError(err) {
    if (err.status === 403) console.warn("Check the key's allowed domains");
    else if (err.status === 429) console.warn("Too many calls right now");
    else console.warn("Address lookup failed", err);
  },
});

Error responses carry CORS headers, so the browser sees the real status code and not just a network error. A browser key has a per-visitor limit of 120 searches a minute. Above it the API answers 429 rate_limit_per_visitor with Retry-After, and the field shows the error message until the user types again.

danadresse-widget.js shows a Danish message per cause: missing or invalid key (401), domain not allowed (403), too many searches (429) and network error. Its onError receives { kind } with unauthorized, forbidden, rate, network or error.

7. See the call the script makes

The script sends only the typed text, the caret position, the type, the limit and fuzzy. The key travels in the X-Api-Key header, and no cookies are sent (credentials: "omit"). You can repeat the call from a terminal:

Shell#widget-request
curl -s "https://api.danadresse.dk/autocomplete?q=Strandvejen%20100%2C%202900&type=adresse&per_side=8&fuzzy=" \
  -H "X-Api-Key: $DANADRESSE_API_KEY"

If this works but the browser fails, the cause is almost always the key's domain list. See Browser keys and allowed domains.

8. Styling, SRI and self-hosting

The list follows prefers-color-scheme. The colours are CSS variables you can override:

CSS#theme-vars
.dda2 {
  --dda2-bg: #ffffff;
  --dda2-fg: #1a1a1a;
  --dda2-active: #fff4d6;
  --dda2-link: #8a4b00;
}

danadresse-autocomplete-1.0.0.js never changes, so you can use Subresource Integrity. Compute the hash of exactly the file you embed and put it in integrity together with crossorigin="anonymous":

Shell#sri-hash
curl -s https://danadresse.dk/static/js/danadresse-autocomplete-1.0.0.js \
  | openssl dgst -sha384 -binary | openssl base64 -A

You may also copy the file into your own static folder. It is MIT-licensed. Keep the version in the file name so you can tell which version you run. Do not use SRI on danadresse-widget.js or on the alias danadresse-autocomplete.js: they are not versioned, and the hash stops matching when the file is updated.

Next steps

Related guides

Markdown source Spotted an error? Tell us API-reference (Swagger)

Try it with a free key

2,000 calls/month on the free plan. No credit card.

Create account →