Building a KVK Number Validation Widget with JavaScript
· KVKBase Team

Building a KVK Number Validation Widget with JavaScript

Step-by-step tutorial for building an embeddable KVK number lookup widget with JavaScript, including event handling, accessibility, and CSS customization.

javascriptwidgettutorial

Building a KVK Number Validation Widget with JavaScript

A KVK number validation widget lets your users type in a Dutch Chamber of Commerce number and instantly see the associated company details. In this tutorial, we will build one from scratch using the KVKBase widget, and then cover how to customize and extend it for your own application.

Quick Start: Using the KVKBase Widget

The fastest path to a working KVK lookup widget is the pre-built KVKBase component. Add a single script tag and a custom element to your page:

<!DOCTYPE html>
<html lang="nl">
<head>
  <meta charset="UTF-8">
  <title>KVK Lookup</title>
</head>
<body>
  <h1>Zoek een bedrijf</h1>

  <script
    src="https://widget.kvkbase.nl/kvk-lookup.js"
    data-api-key="YOUR_API_KEY"
  ></script>

  <kvk-lookup
    placeholder="KVK-nummer invoeren"
    lang="nl"
  ></kvk-lookup>
</body>
</html>

That is all you need for a fully functional lookup widget. The component handles input validation, API calls, loading states, and result display out of the box.

Data Attributes

The widget accepts several data attributes for configuration:

<kvk-lookup
  placeholder="Voer een KVK-nummer in"
  lang="nl"
  show-details="true"
  auto-submit="false"
  theme="light"
></kvk-lookup>
AttributeDefaultDescription
placeholder”KVK nummer”Input placeholder text
lang”nl”Language: “nl” or “en”
show-details”true”Show company details after lookup
auto-submit”true”Automatically look up when 8 digits are entered
theme”light”Color theme: “light” or “dark”

Event Handling

The real power of the widget comes from its event system. You can listen for events to react when a company is found, when an error occurs, or when the user clears the input.

kvkbase:found

Fired when a valid company is found:

const widget = document.querySelector('kvk-lookup');

widget.addEventListener('kvkbase:found', (event) => {
  const company = event.detail;

  console.log('Company found:', company.tradeName);
  console.log('KVK:', company.kvkNumber);
  console.log('Address:', company.address);
  console.log('Active:', company.isActive);
});

The event.detail object contains the full company profile:

{
  kvkNumber: "12345678",
  tradeName: "Example BV",
  legalForm: "Besloten Vennootschap",
  address: {
    street: "Keizersgracht",
    houseNumber: "100",
    postalCode: "1015AA",
    city: "Amsterdam"
  },
  sbiCodes: [
    { code: "6201", description: "Software development" }
  ],
  isActive: true,
  vatNumber: "NL123456789B01"
}

kvkbase:error

Fired when a lookup fails:

widget.addEventListener('kvkbase:error', (event) => {
  const error = event.detail;

  switch (error.code) {
    case 'NOT_FOUND':
      showMessage('No company found with this KVK number.');
      break;
    case 'INVALID_FORMAT':
      showMessage('Please enter a valid 8-digit KVK number.');
      break;
    case 'RATE_LIMITED':
      showMessage('Too many requests. Please try again shortly.');
      break;
    default:
      showMessage('Something went wrong. Please try again.');
  }
});

kvkbase:clear

Fired when the user clears the input:

widget.addEventListener('kvkbase:clear', () => {
  // Reset any form fields that were auto-filled
  document.getElementById('company-name').value = '';
  document.getElementById('address').value = '';
});

Auto-Filling Form Fields

The most common use case is auto-filling a registration or checkout form:

<form id="registration-form">
  <label>KVK Nummer</label>
  <kvk-lookup id="kvk-widget"></kvk-lookup>

  <label>Bedrijfsnaam</label>
  <input type="text" id="company-name" readonly>

  <label>Adres</label>
  <input type="text" id="address" readonly>

  <label>Postcode</label>
  <input type="text" id="postal-code" readonly>

  <label>Plaats</label>
  <input type="text" id="city" readonly>

  <label>BTW-nummer</label>
  <input type="text" id="vat-number" readonly>

  <button type="submit">Registreren</button>
</form>

<script>
  document.getElementById('kvk-widget')
    .addEventListener('kvkbase:found', (event) => {
      const c = event.detail;
      document.getElementById('company-name').value = c.tradeName;
      document.getElementById('address').value =
        `${c.address.street} ${c.address.houseNumber}`;
      document.getElementById('postal-code').value = c.address.postalCode;
      document.getElementById('city').value = c.address.city;
      document.getElementById('vat-number').value = c.vatNumber || '';
    });
</script>

CSS Customization

The widget uses Shadow DOM to encapsulate its styles, but it exposes CSS custom properties for theming:

kvk-lookup {
  --kvk-primary-color: #2563eb;
  --kvk-border-color: #d1d5db;
  --kvk-border-radius: 8px;
  --kvk-font-family: 'Inter', sans-serif;
  --kvk-font-size: 14px;
  --kvk-input-padding: 12px 16px;
  --kvk-background: #ffffff;
  --kvk-text-color: #111827;
  --kvk-error-color: #dc2626;
  --kvk-success-color: #16a34a;
}

For dark mode:

@media (prefers-color-scheme: dark) {
  kvk-lookup {
    --kvk-background: #1f2937;
    --kvk-text-color: #f9fafb;
    --kvk-border-color: #4b5563;
  }
}

Accessibility Considerations

A well-built widget must be accessible. The KVKBase widget includes several accessibility features by default, but here are important points to keep in mind when integrating it:

Labels and ARIA

Always associate a label with the widget:

<label for="kvk-input">KVK Nummer</label>
<kvk-lookup id="kvk-input" aria-label="KVK nummer opzoeken"></kvk-lookup>

Keyboard Navigation

The widget supports full keyboard navigation:

  • Tab to focus the input
  • Enter to trigger the lookup
  • Escape to clear results
  • Arrow keys to navigate search suggestions

Screen Reader Announcements

When a company is found or an error occurs, the widget uses aria-live regions to announce the result to screen readers. You do not need to add anything extra for this — it works out of the box.

Error States

Error messages are associated with the input via aria-describedby, so screen readers announce them in context.

Building a Custom Widget

If you need full control over the UI, you can build your own widget using the KVKBase API directly:

class CustomKvkLookup extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    this.render();
    this.shadowRoot.querySelector('input')
      .addEventListener('input', this.handleInput.bind(this));
  }

  async handleInput(e) {
    const value = e.target.value.replace(/\D/g, '');
    if (value.length !== 8) return;

    const statusEl = this.shadowRoot.querySelector('.status');
    statusEl.textContent = 'Searching...';

    try {
      const res = await fetch(
        `https://api.kvkbase.nl/api/v1/lookup/${value}`,
        { headers: { 'Authorization': `Bearer ${this.getAttribute('api-key')}` } }
      );
      const data = await res.json();

      this.dispatchEvent(new CustomEvent('kvkbase:found', { detail: data }));
      statusEl.textContent = data.tradeName;
    } catch {
      this.dispatchEvent(new CustomEvent('kvkbase:error', {
        detail: { code: 'NETWORK_ERROR' }
      }));
      statusEl.textContent = 'Lookup failed';
    }
  }

  render() {
    this.shadowRoot.innerHTML = `
      <style>
        :host { display: block; }
        input { width: 100%; padding: 8px 12px; font-size: 16px; }
        .status { margin-top: 8px; color: #666; }
      </style>
      <input type="text" maxlength="8" placeholder="KVK nummer"
             aria-label="KVK nummer">
      <div class="status" aria-live="polite"></div>
    `;
  }
}

customElements.define('custom-kvk-lookup', CustomKvkLookup);

Summary

Whether you use the pre-built KVKBase widget or build your own, the key ingredients are the same: clean input validation, responsive API calls, clear event handling, and accessible markup. The KVKBase widget gives you all of this out of the box, while the API lets you build exactly the UI you want.

Start with the widget for rapid integration, then customize with CSS properties and event listeners as your requirements grow.