Styling a native date input into a custom, no-library datepicker
Tags: javascript, html, css, react
Published: 2020-11-09 11:09:08

For [Helppo](https://github.com/codeclown/helppo/) users, I wanted to offer a way to edit date and datetime values more conveniently than with a plain text input. This is of course nothing new; most sites use datepickers for this purpose.

As a developer, there are plenty of ready-made options to choose from. For example, [react-datepicker](https://github.com/Hacker0x01/react-datepicker) (which almost doubled the bundle size in the case of Helppo) with ~720k weekly installs. Or [pikaday](https://github.com/Pikaday/Pikaday) (which has promising principles, although a lot of issues and PRs open) with ~210k weekly installs.

I wanted to see if just a regular `<input type="date">` could do the job. I had a couple of requirements and/or liberties, depending on how you want to look at it:

- Because Helppo should (a) be able to display a date value in any format coming from the user's database, and (b) the format in which the user wants to input a new value should not be restricted, I wanted the datepicker to be a separate element next to a plain text field.

- The datepicker element should be styleable via CSS.

- I didn't consider a time picker necessary. From my experience, time picker UIs are generally more difficult to use than a regular text field. (If Helppo users begin requesting this functionality, then I will of course look into it.)

With these in mind I started experimenting and browsing StackOverflow.

# Default appearance of `<input type="date">`

MDN offers an [incredibly helpful wiki page](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date) around `<input type="date">`, and it includes screenshots of the datepicker on various browsers. I added a couple of mine to the bottom of the list.

One thing I found out is that in Safari (and in IE, I hear) a date input acts like a regular text input, and has no datepicker.

**On Chrome:**

![date-picker-chrome](https://dev-to-uploads.s3.amazonaws.com/i/rxgfmgnwg3avjdp23wrc.png)

Screenshot by [Mozilla Contributors](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date) is licensed under [CC-BY-SA 2.5](https://creativecommons.org/licenses/by-sa/2.5/).

**On Firefox:**

![firefox_datepicker](https://dev-to-uploads.s3.amazonaws.com/i/am7t4q8kjdis38eecz9j.png)

Screenshot by [Mozilla Contributors](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date) is licensed under [CC-BY-SA 2.5](https://creativecommons.org/licenses/by-sa/2.5/).

**On Edge:**

![date-picker-edge](https://dev-to-uploads.s3.amazonaws.com/i/1uflidax0wespb1is6b4.png)

Screenshot by [Mozilla Contributors](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date) is licensed under [CC-BY-SA 2.5](https://creativecommons.org/licenses/by-sa/2.5/).

**On Chrome (MacOS):**

![date-picker-chrome](https://dev-to-uploads.s3.amazonaws.com/i/15oib4gzm53k4jaw9h1u.png)
 
Screenshot by me.

**On Safari (MacOS):**

None! No datepicker for Safari.

![date-picker-safari](https://dev-to-uploads.s3.amazonaws.com/i/9ekth7js5j0k2yy839xr.png)

Screenshot by me.

---

As can be seen in the above screenshots, the default datepicker is a text input where the date is displayed in a locale-specific format. Either clicking on the input, or on a calendar-icon inside it, opens a datepicked popup. This is similar to how datepicker JS libraries work.

To highlight the different components:

![datepicker-components](https://dev-to-uploads.s3.amazonaws.com/i/0h0458hw4wacr3i8p4af.png)

Whereas JS libraries allow for customization of styles and functionality, in the case of `<input type="date">` you don't really get that. There's no toggle to enable week numbers or year dropdowns; the browser will either render such things, or it won't.

# Default functionality of `<input type="date">`

Another concern was if the input value, when read via JavaScript, would be consistent between browsers.

In the case of [Helppo](https://github.com/codeclown/helppo/) I always wanted it in the same format (YYYY-MM-DD), but obviously it wouldn't really matter what the format was as long as it could be consistently parsed.

Luckily this is not a problem! As per the [MDN page](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date), reading `input.value` should result in a consistent format:

```js
const dateInput = document.querySelector('.date-input');
console.log(dateInput.value); // "2020-10-26"
```

This is regardless of the fact that the visible format is locale-based in the input.

To be sure, I tested and verified it in various browsers on MacOS.

Based on this result, we can confidently listen to `onchange` event on a `<input type="date">` and get the same date format back no matter the platform.

# Customizing the input

First of all, if your application needs a datepicker, I would strongly recommend just using `<input type="date">` as seen above! There really should be a specific reason for including a datepicker library when the out-of-the-box solution works so well.

As noted in the beginning, however, for [Helppo](https://github.com/codeclown/helppo/) I had the requirement that the datepicker should be an extra button next to a regular text input. In this fashion:

![datepicker-toggle](https://dev-to-uploads.s3.amazonaws.com/i/khtc0pleepp6gdsrbjxf.png)

So I needed to transform `<input type="date">` into a button-like element. 

---

Our starting point:

1. Some browsers (Firefox, Edge) don't have a toggle, and just open the popup when you click the input
2. Some browsers (Chrome) open the popup when you click a toggle-button
3. Some browsers (Safari, IE) don't open a popup at all

For point #1, we should be able to just make a date input invisible and stretch it on top of the toggle we want to display. That way, when the user clicks the toggle, they actually click the date input which triggers the popup.

For point #2, we should try to do the same but for the toggle-part of the date input. I started searching and landed on [this answer](https://stackoverflow.com/a/51349612/239527) in StackOverflow, which does that for webkit browsers.

For point #3, we should not show the toggle at all because clicking on it won't do anything. Ideally this would be done without user-agent sniffing or any other method of hard-coded browser detection.

# Making it happen (the markup)

Here's how we'll structure the toggle-button in the HTML:

```html
<span class="datepicker-toggle">
  <span class="datepicker-toggle-button"></span>
  <input type="date" class="datepicker-input">
</span>
```

> Note on accessibility: because we're using just spans with no text content, the only actionable element in this piece of HTML is the input. The focus flow of the document is the same as if there just was a regular input here. If you use different kind of elements (e.g. an `<img>`), make sure to ensure your element stays accessible.

And here are the base styles of the wrapper element and the visual button we want to show:

```css
.datepicker-toggle {
  display: inline-block;
  position: relative;
  width: 18px;
  height: 19px;
}
.datepicker-toggle-button {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  background-image: url('data:image/svg+xml;base64,...');
}
```

> Note on `.datepicker-toggle-button`: this is the element you would style entirely based on your application. In this case I'm just using a background-image for simplicity.

At this point we just have the date input visible next to the button-element, as it resides in the DOM:

![initial](https://dev-to-uploads.s3.amazonaws.com/i/u2yrlb5ouei4y8xdqlbj.png)

Here begins the browser-specific CSS for stretching the actionable part of the input over the visible button.

Based on the previously mentioned [SO answer](https://stackoverflow.com/a/51349612/239527):

```css
.datepicker-input {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  cursor: pointer;
  box-sizing: border-box;
}
.datepicker-input::-webkit-calendar-picker-indicator {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  cursor: pointer;
}
```

Works for Chrome (MacOS), which normally has the toggle:

![chrome-css](https://dev-to-uploads.s3.amazonaws.com/i/mqx7hnmxvzg9j6qi55cx.png)

Works for Firefox (MacOS), which doesn't have a toggle:

![firefox-css](https://dev-to-uploads.s3.amazonaws.com/i/3ot4s2r2wqzqz55mra0s.png)

I also asked a friend to verify on Firefox on Windows, and they sent me a similar screenshot to the above.

I wasn't able to verify it on Edge, but based on the fact that on Edge clicking the input triggers the popup, it should work similarly to Firefox because we stretch the invisible input over the whole button.

Finally, I tested it also on iOS Safari, and the datepicker opened as expected:

![ios-safari-css](https://dev-to-uploads.s3.amazonaws.com/i/2i670a7nqcwne46buua1.png)

# Detecting browsers that don't have a datepicker popup

As mentioned above, some browsers (Safari, IE) don't have the datepicker functionality built-in. Instead a date input acts like a regular text input. For these browsers we shouldn't show the button at all, because it would just sit there without doing anything.

I looked into detecting the browser support automatically. Some ideas I researched:

- Detecting the existence of specific shadow DOM. See the below screenshot. It would've been great if we could check the existence of datepicker-specific elements (e.g. `input.shadowRoot.querySelector("[pseudo='-webkit-calendar-picker-indicator']")`), but unfortunately that's not possible in the case of an input element (`shadowRoot` is `null`; read more at [MDN](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot)). I'm not aware of any workaround for this.

![shadow-dom](https://dev-to-uploads.s3.amazonaws.com/i/tx3trvxe54oe58xacxfy.png)

- Reading `document.styleSheets` to figure out if our pseudo-element CSS selector has been successfully applied. Now, this was a wild idea based on no prior code I'd ever seen, and of course it yielded no results. `CSSStyleSheet` does not contain any information about how the style was applied or if it was valid.

- Continued on the previous point… how about trying to read the styles of the pseudo element, and noticing differences in the results based on the browser? I added an obscure enough CSS style for the pseudo-element: `.datepicker-input::-webkit-calendar-picker-indicator { font-variant-caps: titling-caps; }` and then tried to read it: `window.getComputedStyle($0, '::-webkit-calendar-picker-indicator').fontVariantCaps` (also with variations of and the lack of colons). Unfortunately this would always return the style value of the input element, not the pseudo element. This is probably because, again, we can't access the shadow DOM. The above works for e.g. `:before` pseudo-elements, but doesn't seem to work for our use case.

- Checking if `<input type="date">` value is automatically sanitized by the browser. This turned out to be the winner. In hindsight I should've checked this one first, but it seemed obvious to me that this would not work, because I assumed that a date input would still have formatting/sanitization. Turns out that's not the case.

So in the end checking for datepicker support is as simple as setting an input value and checking if the browser accepted it:

```js
const input = document.createElement('input');
input.type = 'date';
input.value = 'invalid date value';
const isSupported = input.value !== 'invalid date value';
```

I verified that this works in Safari.

# Listening for `onchange` event

All that's left is to update the value of a text input when the user chooses a date from the popup. That is very straight-forward:

```js
const textInput = document.querySelector('.text-input');
const dateInput = document.querySelector('.datepicker-input');
dateInput.addEventListener('change', event => {
  textInput.value = event.target.value;
  // Reset the value so the picker always
  // opens in a fresh state regardless of
  // what was last picked
  event.target.value = '';
});
```

An alternative to resetting the date input value is to keep it in sync with the text input, which gets a bit more complicated but is still quite simple:

```js
dateInput.addEventListener('change', event => {
  textInput.value = event.target.value;
});
textInput.addEventListener('input', event => {
  const value = textInput.value.trim();
  dateInput.value = value.match(/^\d{4}-\d{2}-\d{2}$/) ? value : '';
});
```

# GitHub repository

I've packaged this solution into a reusable mini-library here:

- https://github.com/codeclown/native-datepicker

Also published on npm as `native-datepicker`:

- https://www.npmjs.com/package/native-datepicker

The repository contains examples of how to use the code in both a React codebase as well as in plain vanilla JavaScript. A glimpse of the API:

```js
const picker = new NativeDatepicker({
  onChange: value => {
    // ...
  },
});
someElement.appendNode(picker.element);
```

Included is also a ready-made React-component:

```jsx
<NativeDatepicker
  className="custom-class"
  value={value}
  onChange={value => {
    // ...
  }}
/>
```

It is also aware of datetime inputs (i.e. if the text input contains a value like "2020-10-26 23:35:00", the library will replace just the date-portion of it upon change).

It would be cool if you found use for the package. If there's any issues, please file an issue in the GitHub repository!

# Thanks

Thanks for reading!

Thank you also to MDN and the StackOverflow commenters for the building blocks of this exploration.

If you haven't already, feel free to check out [Helppo](https://github.com/codeclown/helppo/), for which I originally built this component. Helppo is a CLI-tool (written in NodeJS) which allows you to spin up an admin interface for your database. Just run `npx helppo-cli --help` to get started.