Building A Birthdate Input Custom Element
In this article we look at a <birthdate-input>
custom element that automatically orders input fields based on the browser locale. Useful for asking your customers to input their birthdate without presenting them with a datepicker.
Why not a datepicker? Because humans remember their birthdate as a string of numbers and the day of the week is not relevant.
Entering those numbers is easier than navigating a date picker to find your birthdate.
I posted about this on Bluesky and figured it’s probably useful to have a birthdate input custom element for this that automatically adapts to the user browser locale.
The Base HTML
The birthdate is defined by a set of labelled number inputs.
We’ll wrap these inputs in a fieldset so they’re grouped as a birthdate.
An optional locale
attribute can be set on the <birtdate-input>
to override the default browser locale.
<fieldset>
<legend>Birthdate</legend>
<birthdate-input locale="en-US">
<label for="day">Day</label>
<input id="day" min="1" max="31" type="number" placeholder="dd" />
<label for="month">Month</label>
<input id="month" min="1" max="12" type="number" placeholder="mm" />
<label for="year">Year</label>
<input
id="year"
min="1800"
max="2025"
type="number"
placeholder="yyyy"
/>
</birthdate-input>
</fieldset>
The Birthdate Input Custom Element
Now we need to register the <birthdate-input>
custom element.
This custom element automatically orders the fields based on the detected locale.
class BirthdateInput extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
const defaultFields = ['day', 'month', 'year'];
// get inputs
const [day, month, year] = Array.from(
this.querySelectorAll('input[type="number"]')
);
this.inputs = [day, month, year];
this.inputRefs = { day, month, year };
// add field keys
for (const [index, input] of Object.entries(this.inputs)) {
input.dataset.key = defaultFields[index];
}
// order parts based on the browser default locale
const dateTimeFormat = new Intl.DateTimeFormat(
this.getAttribute('locale') || undefined
);
const parts = dateTimeFormat.formatToParts();
const order = parts
.filter((part) => defaultFields.includes(part.type))
.map((part) => part.type);
// sort fields
for (const key of order) {
const input = this.inputRefs[key];
// append the label
this.append(this.querySelector(`label[for=${input.id}]`));
// then append the input
this.append(input);
}
// just focussed other field, prevent tabbing out of it by accident
let preventTab = false;
this.addEventListener('keydown', (e) => {
if (e.key === 'Tab' && preventTab) {
e.preventDefault();
return;
}
});
// auto jump to next field when user inputs numbers
this.addEventListener('keyup', (e) => {
// easier prop access
const input = e.target;
const { value, placeholder, dataset } = input;
const currentIndex = order.indexOf(dataset.key);
// move to previous field
if (e.key === 'Backspace' && value.length === 0) {
const previousField = this.inputRefs[order[currentIndex - 1]];
if (!previousField) return;
previousField.focus();
}
// not a number, ignore
if (!/[0-9]/.test(e.key)) return;
// not filled out completely
if (value.length !== placeholder.length) return;
// get next field
const nextField = this.inputRefs[order[currentIndex + 1]];
if (!nextField) return;
// focus this field
preventTab = true;
setTimeout(() => (preventTab = false), 250);
nextField.focus();
});
}
disconnectedCallback() {
// restore original order
for (const input of this.inputs) {
this.append(input);
}
}
}
customElements.define('birthdate-input', BirthdateInput);
Demo field
This is a live demo of the <birthdate-input>
component. The order of the fields is determined by your browser default locale.