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.

Birthdate

I share web dev tips on Bluesky, if you found this interesting and want to learn more, follow me thereBluesky

Or join my newsletter

More articles More articles