Simple web component for selecting ranges of local dates.
Note: Documentation written by Anders Gustafson, revised .
When I needed a solution for easily picking ranges of local dates, I could not find any perfect alternatives out in the Internet jungle. So I decided to build my own web component, nnm-local-date-range-picker
. Of course, it would also be a fun exercise building your own solution, trying to learn various things about web components along the road.
During my investigations of time zones, I have come to the conclusion that many parts of date handling are often way too complex. Selecting dates within a web page should be simple, whether it is a single date or a date range.
Give this NNM Local date range picker a try. On first look, you may think that this component is not simple and contains too much information and details, but I truly think that I can prove you wrong and, when you understand the reasoning, you will actually find the concepts and user experiences quite convenient, intuitive, helpful, unambiguous, appealing and simple.
I have been inspired from some already existing date (range) pickers, especially the Date Range Picker component which I (still) think is among the best of them. Some existing solutions are good, but everyone lacks some functionality that I needed and many of them also felt a bit too complex or are ambiguous and confusing.
My guiding principles for this project have been don't complicate things and don't make me think. Hopefully this will be seen in the result.
Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.
You should be able to select a range of local dates. The most important word in the previous sentence is "local" and you should pronounce local date range picker as "local-date range-picker" and not "local date-range-picker". We should also not complicate things by handling time zones.
The resulting range of local dates, should consist of simple strings, defining the local dates in the ISO 8601 extended format (yyyy-mm-dd
). This format is used in order to avoid ambiguity and for easy reading. There is no need for handling complex date objects, strange epoch values (sometimes defined in milliseconds and sometimes defined in seconds) or different string formats. No time part is needed in the string, we only need the date part (a local datetime range picker will be another project).
Note: In my humble opinion, there are way too many solutions in the wild, that instead of being date range pickers, they are actually datetime range pickers. You really need to understand which of those variants you want for your project, because the use-cases are completely different, and if you make a bad decision your users will often be extremely confused.
The primary purpose of the component should be to easily help the user, with as few clicks (or keystrokes) as possible, to pick a local date range, for example 2021-11-20 - 2022-01-05
, and to display the selected range in an understandable way. Everything else is secondary.
Note: I emphasize, that the result returned from the component should be two simple strings (with format yyyy-mm-dd
) defining the local date endpoints. If returning anything else you only make things way more complicated than needed.
The component should be focused on selecting ranges in the "near" past/future. Selecting date ranges years away from today is not key functionality. It is more important that you can select date ranges in the nearest weeks/months/quarters in a convenient way.
Note: Another thing that I think we all are bad at, is to use different solutions for different tasks/contexts. For example, there should be totally different requirements for a date range picker, if the main purpose is to select ranges far from now, compared to this solution. Often, there is not one solution to rule them all.
I advocate that we should distinguish local dates from zoned dates. You can read more about my reasons in the section about converting date and time values. A date range picker should be used to select local dates. In some applications, these local dates may sometimes have to be converted to zoned dates, but that complexity should not be part of a picker component, that is application specific functionality.
A local date is also known as a plain date or floating time.
yyyy-mm-dd
?Absolutely not! The reason is that such functionality will make the component more complex, but with no benefits at all. If you, for some strange reason, want to use other formats, that added complexity should be your problem.
Note: You can read more details about my reasoning in the "Formats for clients" section in The golden timestamp rules.
At the moment, the currently selected range is displayed in a hardcoded format (eg Wed 23 March
). In future versions this may be a configurable option, but only for the actual user and not for the developer or product owner.
Note: The format that I as a user think is most suitable for different things, is not something that should be decided by anyone else. If I think that it is easier to understand 2022-03-23
, Wed 23 March
, March 23
, 23/3
or something else, should always be my own decision. If there is a possibility to define formats for how date and time are presented to the user, that choice should only be visible for the user.
Note: One thing I really care about, is that all values that someone present, should be unambiguous (it can be dates, times, costs, temperatures, or whatever). That is also the reason why I argue that always using (ISO) formats is a good thing. I have seen way to many documents and presentations during the years, where the audience completely misinterpret the values, just because the author took a shortcut.
Secondary functionality that I aimed for in the first version.
When selecting ranges, it should be possible to select the endpoints in any order. No annoying errors like "You need to select the start date before the end date".
Note: I think there are too many solutions that handle this poorly, where you must select the start date first.
It would be enough to support English as language. Trying to support several languages often ends up in bad user experience and bugs.
Supports ISO calendar. There is a standard, let us use that one. No need to complicate things more than necessary.
Since we are using ISO, a week starts on Mondays and ends on Sundays.
An easy and convenient way of selecting the most common ranges, that probably will be used more than 95% of the time. It should also be explicitly indicated when your selected range matches one of these common ranges.
Each button in a specific row should define roughly the same amount of days. There should be three columns where the left define ranges in the past, the right defines ranges in the future, and the middle defines ranges containing current date. The left and right buttons may sometimes include current date, the middle button always includes current date.
The common ranges to be in a quick list could be things like:
Ranges that should be indicated, should be the ones in the quick list plus things like Q1, Q2, Q3, Q4, H1 and H2.
Yesterday, today and tomorrow contain one day.
Previous week, current week and next week contain seven days.
Previous month, current month and next month are dynamic and contain 28-31 days.
Since the "-", "±" and "+" ranges should always include current date, they are a bit special. The "±" range should contain the same number of days before and after current date, and therefore the number of days for these ranges must be an odd number (so "-2 weeks" contains 14 days, but "±1 week" contains 13 days).
Displaying three consecutive months, so it will be easier to pick longer ranges (especially when picking ranges crossing months). You will avoid many extraneous clicks when displaying three months, and in my humble opinion you also get a more appealing and balanced user interface compared to showing two months.
Under normal circumstances, you will always see at least four weeks before and four weeks after current date, when three months are displayed. If you only display two months, you may sometimes have zero(!) days before or after current date, which I think is quite annoying.
Yes, when selecting single local dates, seeing one month may be sufficient, since it is a single value. But when selecting ranges, I truly think that you benefit of seeing as much of your range as possible. Using three months feels like a good compromise for ranges.
Note: For desktops, laptops and tablets, the available space for displaying three months is usually not a problem. So I am really surprised that there aren't more date range picker solutions displaying three months on "large" monitors.
By displaying three months instead of two, you actually double the number of days that a range can contain and always be visible.
Note: The examples below are valid during non-leap years, i.e. when February contains 28 days.
When you display one month, you can only be certain that ranges of one (1!) day can be fully shown. For example, the range 31 January to 1 February, which is only two days, cannot be fully shown. The max number of days that can be shown is 31.
When you display two months, you can only be certain that ranges of 29 days can be fully shown. For example, the range 31 January to 1 March, which is 30 days, cannot be fully shown. The max number of days that can be shown is 62 (for example July and August).
When you display three months, you can always be certain that ranges of 60 days can be fully shown. For example, the range 31 January to 1 April, which is 61 days, cannot be fully shown. The max number of days that can be shown is 92 (for example June, July and August).
Note: There is quite a difference being able to easily display ranges consisting of 1 day, 29 days or 60 days.
The actual number of days in the selected range should be explicitly displayed, so the user directly understands how long the range is.
Note: Leaving this calculation as an exercise to the users, is a terrible user experience in my opinion.
Note: The only exception from displaying number of days is when you have selected one (1) day, since that special range will be easily understood.
When using a date range picker for selecting hotel nights, it may be better to show the difference in days (which equals the number of nights). But for most use cases, I would argue that it is better to indicate the actual number of days.
When seeing date pickers that count 2022-01-22 - 2022-01-23
as one day, I always get confused and many questions arise:
2022-01-22
really be represented as zero days?
If you have a selected range of local dates, and you are doing database queries, you should include the complete days in the range (otherwise your component should be a datetime range picker instead). So if you select the date range 2021-11-20 - 2022-01-05
, you should interpret it as 2021-11-20T00:00 - 2022-01-05T23:59
when you search. Of course, you may have to interpret this range to be in a specific time zone, and then you need to document this in a clear and concise way to avoid confusion for the user.
Note: Of course, if you have a date range picker for selecting hotel nights, you should indicate that with the number of nights (and not number of days), but I hope that is obvious to everyone now.
What time zone is used for the selected dates? The short answer is none. A little longer answer in the next paragraph.
The component should not have to care about time zone complexity. The local date matching "today" should/may be decided from the browser settings, but from that moment on, it is all about displaying "dumb" calendars. Displaying calendars to a user, has nothing to do with time zones, it is just an endless list of consecutive months.
As soon as you mix in time zones, you only complicate things and add problems, without getting any benefits. Take my word for it.
Note: I have seen so many complex solutions where you need to keep track of confusing time zones and/or strange epoch values, just to be able to use a date (range) picker. I truly think that is the completely wrong way to go.
Support for min/max configurations, so out-of-range dates can be disabled.
Using min/max can also be a workaround to easily make it possible to select dates in the "far" past/future.
No annoying "overlapping" of days between calendar months. A month should only display days belonging to that month. It is not smart to clutter the user interface with those "overlaps". I have not yet seen a good solution with this type of duplication.
Note: I think this overlapping of days is one of the biggest problem with many date range pickers, since you have a hard time understand the user interface and often the solutions are ambiguous.
Note: The only mitigating circumstance to have overlaps may be when scrolling vertical through months.
Every month should always have room for exactly six weeks, even if a month sometimes just consists of four weeks.
Note: It is really no problem since the picker displays three months, but even if it should have displayed only one month, I would go for six weeks, to avoid a flickering interface.
Day of week for the endpoints of the selected range should be explicitly indicated. It is often of great help when you immediately can see the day of week and not just the date.
Do not clutter space by displaying year for dates in current year, but be explicit for other years. The user will easily understand dates within current year and will be thankful for a cleaner user interface.
The same with the months. Skip displaying year for the months in current year, but be explicit for other years.
Some way of using keys to change months.
It would also be nice to have the possibility to modify the selected range in some way. It can be to move, widen or narrow the range. Currently, trying different solutions for this.
I think that one important thing is to easily understand the selected date range value. To help the user I think the following is needed:
So if you select the date range 1 December to 31 December, and that happens to also be previous month, you will need to display the following information:
Wed 1 December 2021 - Fri 31 December 2021 (31 days) Previous month
At the same time, the most common range selections will usually be quite narrow. If you select a range in current year, which doesn't match any quick list option:
Sat 25 May - Thu 13 June (20 days)
Note: At the moment the displayed value is written in a single row, but perhaps it will be divided in several rows in future versions.
Note: I am also surprised that there aren't more date range picker solutions displaying more information on "large" monitors for the collapsed value. The user should not have to expand the input field to get relevant and crucial information about the selected date range. For sure, I don't think it is good enough to only display 2021-12-01 - 2021-12-31
.
Note: Displaying the number of days is the default. But as discussed earlier, in future versions it should be possible to configure if you want to display the number of nights instead.
Functionality that was introduced in an improved version.
There are situations when a user wants a range, but only cares about the lower or upper endpoint. A date range picker should support that and here it is handled by implicit ranges. This is functionality that I don't see often in the date range pickers out in the wild.
Easier to select ranges when you just want to define either the lower endpoint or the upper endpoint. The dropdown list (in the bottom left corner) contains the options "Range", "From" and "Until".
The default option is "Range", where you select an explicit range. You need to define both endpoints.
By changing the dropdown list to either "From" or "Until", one of the endpoints is implicitly defined for you, and you only have to select the other endpoint. The default is to use negative/positive infinity as the implicit endpoint. If you have min/max boundaries configured, the corresponding configured min/max date will be used as the implicit endpoint.
Note: In current version (1.4.0), infinity is represented by special local date constants. Negative infinity uses 0000-01-01
and positive infinity uses 9999-12-31
. This may be changed in the future.
When an implicit endpoint is negative/positive infinity, the selected range can be displayed a bit different. For example, "From Sun 25 August" or "Until Sun 25 August".
Note: Of course, there is no need to display number of days for these infinite ranges.
Note: When in "From"/"Until" mode, the Quick list is disabled. Keyboard support for moving, widening and narrowing the range is also disabled.
Some functionality that hopefully will be implemented in the coming versions:
Showing the progress and enhancements of the component.
Feedback is always appreciated. If you have questions, suggestions for improvements, find errors, please see the contact details at information page.
nnm-local-date-range-picker
web component.
As always, delivered as is, with the best of intentions, but no guarantees.