v1.2.0
  1. ds
  2. Components
  3. Calendar and date range

Calendar and date range

A month grid for dates and ranges, and the time range picker.

Date

October 2026
Code
<div class="cal">
  <div class="cal-month">
    <div class="cal-head">
      <button class="btn icon xs ghost" aria-label="Previous month">…</button><b>October 2026</b><button class="btn icon xs ghost" aria-label="Next month">…</button>
    </div>
    <div class="cal-grid" role="grid" aria-label="October 2026">
      <span class="dow" aria-hidden="true">Mo</span> …
      <span data-out aria-hidden="true">28</span> …
      <button disabled aria-label="11 October 2026">11</button>
      <button aria-current="date" aria-label="12 October 2026">12</button>
      <button aria-selected="true" aria-label="16 October 2026">16</button> …
    </div>
  </div>
</div>

A month on a seven-column grid, weeks from Monday. Today is in the accent with a dot under it; the chosen date is filled. Dates that cannot be picked, here anything before today for a scheduled backfill, are faint and struck through. Days from the next and previous month fill the grid but are not buttons.

Range

October 2026
November 2026
27 Oct 2026 to 4 Nov 2026 · 9 days
Code
<div class="cal">
  <div class="cal-months">
    <div class="cal-month">…</div>
    <div class="cal-month">…</div>
  </div>
  <div class="cal-foot"><span>27 Oct 2026 to 4 Nov 2026 · 9 days</span><div class="btn-row">…</div></div>
</div>

<button data-range="start" aria-selected="true">27</button>
<button data-range="mid">28</button> …
<button data-range="end" aria-selected="true">4</button>

Two months side by side. The ends are filled and the days between are washed edge to edge, rounding where a week wraps. Click a start and an end to try it. The footer says the range in words with its length, because "9 days" is what people check.

Time range

Absolute range

Relative like now-6h or a date like 2026-10-12 09:00. Times are UTC.

Show code
<div class="btn-row"><button class="btn sm" type="button"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>Last 24 hours<svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg></button><button class="btn sm icon" type="button" aria-label="Refresh"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/></svg></button></div>
<div class="trange">
  <div class="trange-abs">
    <span class="label">Absolute range</span>
    <label class="fld"><span>From</span><input class="inp mono" value="now-24h" /></label>
    <label class="fld"><span>To</span><input class="inp mono" value="now" /></label>
    <p class="hint">Relative like <code>now-6h</code> or a date like <code>2026-10-12 09:00</code>. Times are UTC.</p>
    <button class="btn sm primary" type="button">Apply</button>
  </div>
  <ul class="trange-rel" role="listbox" aria-label="Relative ranges">
    <li><button class="mi" type="button" role="option" aria-checked="false"><span>Last 5 minutes</span></button></li>
    <li><button class="mi" type="button" role="option" aria-checked="false"><span>Last 15 minutes</span></button></li>
    <li><button class="mi" type="button" role="option" aria-checked="false"><span>Last 1 hour</span></button></li>
    <li><button class="mi" type="button" role="option" aria-checked="false"><span>Last 6 hours</span></button></li>
    <li><button class="mi" type="button" role="option" aria-checked="true"><span>Last 24 hours</span></button></li>
    <li><button class="mi" type="button" role="option" aria-checked="false"><span>Last 7 days</span></button></li>
    <li><button class="mi" type="button" role="option" aria-checked="false"><span>Last 30 days</span></button></li>
    <li><button class="mi" type="button" role="option" aria-checked="false"><span>Today so far</span></button></li>
    <li><button class="mi" type="button" role="option" aria-checked="false"><span>This week so far</span></button></li>
  </ul>
</div>

The picker every dashboard needs: quick relative ranges on the right, an absolute range on the left that also accepts expressions like now-6h. It is a floating panel with the menu shadow, opened from a button that always shows the current range.

API

ClassOnWhat it does
.caldivThe surface. Holds one .cal-month, or several in .cal-months, and an optional .cal-foot.
.cal-headdivPrevious button, month name, next button.
.cal-griddiv[role=grid]Seven columns: .dow heads, then a button per day.
aria-current="date"buttonToday.
aria-selected="true"buttonA chosen date, filled with the accent.
data-rangebuttonstart, mid or end of a range.
data-outspanA day from the next or previous month.
.trangedivTime range picker: .trange-abs on the left, .trange-rel list of .mi on the right.
Do
  • Show the time zone wherever a time is entered.
  • Disable dates that cannot work instead of rejecting them after.
  • A text field next to the calendar for people who know the date.
Don't
  • A calendar for a date of birth. Use three fields.
  • Ranges without their length.
  • Weeks starting on Sunday in one place and Monday in another.

Accessibility

  • Use one tab stop for the grid and arrow keys to move between days; Page Up and Page Down change month. Each day button has a full label like "16 October 2026".
  • Announce the chosen range in a live region, and keep the footer text in sync.