Date picker
A field or a button that opens a calendar. Choose one date or a range, type a date, or take a preset.
A text field with an empty panel
Focus or a click on the field opens the calendar. A date can also
be typed as YYYY-MM-DD.
<div class="field" style="max-width: 20rem">
<label for="dp-basic">Expiration date</label>
<div class="date-picker">
<input class="input" type="text" id="dp-basic" placeholder="YYYY-MM-DD">
<div data-popover aria-hidden="true"></div>
</div>
</div>
A page loads the calendar library itself, before the Tsunami script
The calendar is
Vanilla Calendar Pro
3.4.0 (MIT licence). It is not in tsunami.min.js: a
page that has a .date-picker loads
dist/vanilla-calendar.min.js with its own
<script> first. A page with no date picker does
not load it and is not affected. With the script missing, each
.date-picker logs an error and stays an empty field.
The library's own stylesheet is not used: the calendar is drawn by
tsunami.css.
<script src="https://cdn.jsdelivr.net/gh/Djirasak/tsunami-framework@v0.1.3/dist/vanilla-calendar.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/Djirasak/tsunami-framework@v0.1.3/dist/tsunami.min.js"></script>
The field's value attribute is the starting date
<div class="field" style="max-width: 20rem">
<label for="dp-value">Created from</label>
<div class="date-picker">
<input class="input" type="text" id="dp-value" value="2026-05-29" placeholder="YYYY-MM-DD">
<div data-popover aria-hidden="true"></div>
</div>
</div>
data-format="long" shows a written date, chosen from
the calendar only
The field is read only: a written date such as
May 29, 2026
cannot be typed.
<div class="field" style="max-width: 20rem">
<label for="dp-long">Expiration date</label>
<div class="date-picker" data-format="long">
<input class="input" type="text" id="dp-long" value="2026-05-29" placeholder="Select a date">
<div data-popover aria-hidden="true"></div>
</div>
</div>
data-mode="range" picks a start and an end date
Two clicks choose the range. Two months show side by side from
768px wide, one below that. A range is typed as
start to end.
<div class="field" style="max-width: 24rem">
<label for="dp-range">Date range</label>
<div class="date-picker" data-mode="range">
<input class="input" type="text" id="dp-range" value="2026-08-27 to 2026-09-25" placeholder="YYYY-MM-DD to YYYY-MM-DD">
<div data-popover aria-hidden="true"></div>
</div>
</div>
A button shows the date, or data-placeholder while none is chosen
The control can be a button holding a span. The span
shows the chosen dates, a range with an arrow between its two
ends, or the root's data-placeholder while there are
none. The hidden input carries the starting value. A date cannot
be typed.
<div class="date-picker" data-placeholder="Select a date" style="max-width: 16rem">
<button type="button" class="btn" aria-expanded="false"><span>Select a date</span><i class="icon icon-chevron-down" aria-hidden="true"></i></button>
<div data-popover aria-hidden="true"></div>
<input type="hidden" name="expires">
</div>
Presets are ready-made dates beside the calendar
The page writes a data-presets group of buttons in
the panel. A preset's value is the dates it chooses,
written by the server, so the script works out no dates itself.
Choosing one closes the panel, and the calendar stays for dates of
one's own. From 768px wide the presets are a column after the
calendar; below that they are two columns under it.
<div class="date-picker" data-format="long" data-placeholder="Select a date" style="max-width: 16rem">
<button type="button" class="btn" aria-expanded="false"><span>Select a date</span><i class="icon icon-chevron-down" aria-hidden="true"></i></button>
<div data-popover aria-hidden="true">
<div data-presets role="group" aria-label="Quick dates">
<button type="button" value="2026-10-06" aria-pressed="false">Today</button>
<button type="button" value="2026-10-07" aria-pressed="false">Tomorrow</button>
<button type="button" value="2026-10-13" aria-pressed="false">In a week</button>
</div>
</div>
<input type="hidden" name="due">
</div>
One control for a period: presets, or dates from the calendar.
data-format="medium" writes the dates with a short
month
The preset that matches the chosen dates has
aria-pressed="true". The page writes it for the
starting value; the script keeps it up to date.
<div class="date-picker" data-mode="range" data-format="medium" style="max-width: 18rem">
<button type="button" class="btn" aria-expanded="false"><span>Sep 7, 2026 → Oct 6, 2026</span><i class="icon icon-chevron-down" aria-hidden="true"></i></button>
<div data-popover aria-hidden="true">
<div data-presets role="group" aria-label="Period">
<button type="button" value="2026-10-06 to 2026-10-06" aria-pressed="false">Today</button>
<button type="button" value="2026-09-30 to 2026-10-06" aria-pressed="false">Last 7 days</button>
<button type="button" value="2026-09-07 to 2026-10-06" aria-pressed="true">Last 30 days</button>
<button type="button" value="2026-07-09 to 2026-10-06" aria-pressed="false">Last 90 days</button>
</div>
</div>
<input type="hidden" name="period" value="2026-09-07 to 2026-10-06">
</div>
data-min and data-max limit the dates that can be chosen
Days outside the limits are dimmed and cannot be chosen. A typed
date outside them marks the field aria-invalid.
<div class="field" style="max-width: 20rem">
<label for="dp-limits">Delivery date</label>
<div class="date-picker" data-min="2026-10-05" data-max="2026-11-15">
<input class="input" type="text" id="dp-limits" placeholder="YYYY-MM-DD" aria-describedby="dp-limits-help">
<div data-popover aria-hidden="true"></div>
</div>
<small id="dp-limits-help">Between October 5 and November 15, 2026.</small>
</div>
The input's data-size sets the height; the width is the root's
<div class="stack" data-direction="row">
<div class="date-picker" style="width: 9rem">
<input class="input" data-size="sm" type="text" placeholder="YYYY-MM-DD" aria-label="Small date">
<div data-popover aria-hidden="true"></div>
</div>
<div class="date-picker" style="width: 10rem">
<input class="input" type="text" placeholder="YYYY-MM-DD" aria-label="Default date">
<div data-popover aria-hidden="true"></div>
</div>
<div class="date-picker" style="width: 11rem">
<input class="input" data-size="lg" type="text" placeholder="YYYY-MM-DD" aria-label="Large date">
<div data-popover aria-hidden="true"></div>
</div>
</div>
A hidden input carries the value for the form, always as YYYY-MM-DD
With data-format the text field shows a written date,
so the form reads the hidden input, which is not shown.
<form class="stack" style="max-width: 20rem" id="dp-form" onsubmit="event.preventDefault(); document.getElementById('dp-form-result').textContent = 'Sent: expires=' + new FormData(this).get('expires')">
<div class="field">
<label for="dp-form-field">Expiration date</label>
<div class="date-picker" data-format="long">
<input class="input" type="text" id="dp-form-field" placeholder="Select a date">
<div data-popover aria-hidden="true"></div>
<input type="hidden" name="expires" value="2026-12-31">
</div>
</div>
<button type="submit" class="btn" style="align-self: flex-start">Save</button>
<small id="dp-form-result">Nothing sent yet.</small>
</form>
Inside a dialog the calendar opens above the dialog, and Escape closes the calendar first
<button type="button" class="btn" commandfor="dp-dialog" command="show-modal">Change expiration</button>
<dialog class="dialog" id="dp-dialog" aria-labelledby="dp-dialog-title" closedby="any">
<header>
<h2 id="dp-dialog-title">Change expiration date</h2>
<button type="button" class="btn" data-variant="ghost" data-size="icon" commandfor="dp-dialog" command="close" aria-label="Close">
<i class="icon icon-x" aria-hidden="true"></i>
</button>
</header>
<div class="field">
<label for="dp-dialog-field">Expiration date</label>
<div class="date-picker">
<input class="input" type="text" id="dp-dialog-field" placeholder="YYYY-MM-DD">
<div data-popover aria-hidden="true"></div>
</div>
</div>
<footer>
<button type="button" class="btn" commandfor="dp-dialog" command="close">Cancel</button>
<button type="button" class="btn" data-variant="primary" commandfor="dp-dialog" command="close">Save</button>
</footer>
</dialog>
Invalid, and disabled
A starting value that is not a date stays in the field as written,
and the page's own aria-invalid and error text stay.
The script clears aria-invalid once a valid date is
typed or chosen.
<div class="stack" style="max-width: 20rem">
<div class="field">
<label for="dp-invalid">Start date</label>
<div class="date-picker">
<input class="input" type="text" id="dp-invalid" value="2026-02-30" placeholder="YYYY-MM-DD" aria-invalid="true" aria-describedby="dp-invalid-error">
<div data-popover aria-hidden="true"></div>
</div>
<small id="dp-invalid-error" role="alert">Enter a date as YYYY-MM-DD.</small>
</div>
<div class="field">
<label for="dp-disabled">End date</label>
<div class="date-picker">
<input class="input" type="text" id="dp-disabled" value="2026-03-15" placeholder="YYYY-MM-DD" disabled>
<div data-popover aria-hidden="true"></div>
</div>
</div>
</div>
The page reads the dates from the change event, and sets them through value
<div class="stack" style="max-width: 24rem">
<div class="date-picker" id="dp-events" data-mode="range">
<input class="input" type="text" placeholder="YYYY-MM-DD to YYYY-MM-DD" aria-label="Date range">
<div data-popover aria-hidden="true"></div>
</div>
<div class="stack" data-direction="row" data-gap="sm">
<button type="button" class="btn" data-size="sm" onclick="document.getElementById('dp-events').value = '2026-10-01 to 2026-10-31'">Set October</button>
<button type="button" class="btn" data-size="sm" onclick="document.getElementById('dp-events').value = ''">Clear</button>
<button type="button" class="btn" data-size="sm" onclick="document.getElementById('dp-events').open()">Open</button>
</div>
<small id="dp-events-result">No dates yet.</small>
</div>
<script>
document.addEventListener("DOMContentLoaded", () => {
const picker = document.getElementById("dp-events");
const result = document.getElementById("dp-events-result");
picker.addEventListener("change", (event) => {
result.textContent = event.detail.value === ""
? "Cleared."
: "Chosen: " + event.detail.dates.join(" and ");
});
});
</script>
Other library options go through mount()
Pass any option of Vanilla Calendar Pro when starting the date
picker by hand. The options are laid over the ones the attributes
set. mount() starts it again if it already has one.
tsunami.datePicker.mount(document.getElementById("delivery-date"), {
firstWeekday: 0,
disableWeekdays: [0, 6],
});
API
The members are on the .date-picker element itself;
the functions live on window.tsunami.datePicker.
| Member | Type | Description |
|---|---|---|
value |
property |
Read: the chosen date, or start to end for a
range, as YYYY-MM-DD; an empty string for
none. Set: takes the same text; an empty string clears it,
and so does text that is not a date.
|
open() |
method | Opens the calendar. |
close() |
method | Closes it. |
calendar |
property, read only |
The library's Calendar object, for anything
its own API offers.
|
tsunami.datePicker.mount(el, options?) |
function |
Starts a date picker on a
.date-picker element, with the library
options laid over the attributes. Throws when Vanilla
Calendar Pro is not loaded.
|
tsunami.datePicker.get(el) |
function |
The library's Calendar of that element, or
null.
|
tsunami.datePicker.destroy(el) |
function | Takes the calendar away and removes the members above. |
Events
| Event | On | Detail | Description |
|---|---|---|---|
change |
.date-picker |
{ value, dates } |
Bubbles. Sent when the dates change, by a click, by a
preset, by typing or by setting value.
value is the text; dates is a
list of one or two. The text field's own
change is stopped, so a listener sees this
one only. An input event also bubbles from
the field that holds the value, so a form and a save bar
notice the edit.
|
Keyboard
| Key | Description |
|---|---|
| Tab | From the field or the button into the calendar, through its controls and then the presets, then out, which closes it. |
| Arrow keys | Move between the days. |
| Enter, Space | On a day, chooses it. On the button, opens or closes the calendar. On a preset, chooses its dates. |
| Escape | Closes the calendar and returns to the field or the button. A dialog around it stays open. |
Accessibility
The control gets aria-haspopup="dialog" and
aria-controls; a button also gets
aria-expanded. The panel is a dialog with an
aria-label and is aria-hidden="true"
until it opens. Give the text input a visible label (or an
aria-label). For an error, set
aria-invalid="true" on the input and point
aria-describedby at the error text, which has
role="alert". The days are buttons with a full date
as their label, and the focus ring is the accent colour. Write
role="group" and an aria-label on the
presets.
Vanilla Calendar Pro
3.4.0 is MIT licensed
(vendor/vanilla-calendar/LICENSE.txt). It is a
separate file, dist/vanilla-calendar.min.js, not part
of tsunami.min.js.
Attributes
| Attribute / element | Values | Description |
|---|---|---|
.date-picker |
The wrapper. It holds the control, an empty
[data-popover] panel and, optionally, a
hidden input.
|
|
data-mode |
range, on .date-picker |
A start and an end date, chosen with two clicks. Two months show side by side from 768px wide, one below that. Without it, one date. |
data-format |
long, medium, on
.date-picker
|
The control shows a written date and takes none typed:
May 29, 2026for long, Sep 7, 2026for medium. Without it the control shows YYYY-MM-DD, and a field takes it typed.
|
data-min |
YYYY-MM-DD, on .date-picker
|
The first date that can be chosen. |
data-max |
YYYY-MM-DD, on .date-picker
|
The last date that can be chosen. |
value |
YYYY-MM-DD or start to end, on
the text input or the hidden input
|
The starting date: 2026-05-29 for a date,
2026-05-01 to 2026-05-29 for a range. The
hidden input's value wins when there is one. A date picker
with a button starts from the hidden input.
|
data-placeholder |
text, on .date-picker |
With a button: the text the button shows while no date is
chosen. With dates it shows them, a range as
start → end. |
data-empty |
present or absent, set by the script |
Present on .date-picker while there are no
dates, so a button's text can be dimmed.
|
data-presets |
on a div in the panel |
Holds the presets: one button for each ready-made date or
range, shown beside the calendar. Give it
role="group" and an aria-label.
|
value |
YYYY-MM-DD or start to end, on a
preset's button
|
The dates the preset chooses, written as the starting value is. |
aria-pressed |
true, false, on a preset's
button
|
true on the preset whose value is the chosen
dates. The page writes it for the starting value; the
script keeps it up to date.
|
data-size |
sm, lg, on the text input |
The height of the field: 28px, 36px (default) or 40px. |
aria-invalid |
true, on the text input |
The field gets a red border, as an input does. The script
sets it on a typed text that is not a date, or outside
data-min and data-max.
|
data-date-picker-initialized |
true, set by the script |
On .date-picker once the calendar has
started.
|