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.
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>
| Attribute | Default | Description |
|---|---|---|
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.