Date Range Picker Calendar

£5

A date field that opens a calendar popover: click a start day, move across the grid to preview the range, and the selection fills in from start to end with a staggered sweep.

advanced
4 more details
staggerkeyboard-navigationmicro-interactiondialog-animation
Date Range Picker Calendar - UI component preview

This element reads better at your own screen size than in the frame below:

About this component

A range picker that shows the range being made. The panel lands first and its contents follow a frame later; moving across the grid previews the span under the pointer; and when the end day is chosen the wash fills in cell by cell from the start date to the end date, so the eye follows the period it just picked rather than finding it already there.

Read the full effect overview

Month navigation slides the grid sideways and lets the day cells settle behind it, in the direction of travel, which is what turns a month change from a redraw into a movement. One month or two sit side by side, and below 720px it always renders one, whatever it has been asked for.

Every date in the script is a plain year, month and day record built from numbers. Nothing is parsed from a string, so the grid cannot slide a column sideways in a timezone west of Greenwich, and the component carries no locale assumptions beyond the month and weekday names at the top of the file. Selecting a range and applying it fires one change event on the component root with ISO date strings.

What's included

9 items
  • Click a start day, move across the grid to preview the range, click an end day to set it
  • The range fills in from start to end with a staggered sweep, and the endpoint pops as it lands
  • Month navigation slides the grid horizontally while the day cells settle behind it
  • One month or two side by side, and always one below 720px
  • Presets column: today, last 7 days, last 30 days, this month
  • Minimum and maximum dates plus individually blocked days, all as data- attributes read live
  • role="dialog" popover over a role="grid" calendar, with a roving tabindex, arrow keys, PageUp/PageDown, Home/End, Enter and Escape
  • One change event on the root carrying start and end as ISO strings
  • Light and dark themes on one set of custom properties

Perfect for

4 use cases
  • Analytics and reporting views where a period drives everything below it
  • Booking and availability forms that need blocked days and a minimum date
  • Admin filters where a range is chosen far more often than a single date
  • Any product that currently ships two separate date inputs side by side

How it works

4 sections

The fill sweep

Each day cell carries a wash layer under its number. Choosing the end date collects every cell between the two endpoints in date order and sweeps their washes open from the left edge with a stagger sized to the length of the range, so a short span fills briskly and a long one still finishes inside a third of a second. Every cell is left clean afterwards, because a cell that keeps a leftover transform mis-measures the next time the grid is rebuilt.

Month navigation

A month change rebuilds the grid and then animates it: the track slides in from the direction of travel while the day cells fade up in a stagger that runs from the leading edge, so going forward reads left to right and going back reads right to left. The popover opens the same way in two beats, the panel first and its contents a moment behind it.

Dates without a parser

Dates are year, month and day records compared as one sortable number. The browser's date object is asked only what weekday a day falls on and how long a month is; nothing is ever built by parsing a string, which is what shifts an ISO date to the previous day in any timezone behind UTC. The first day of the week is a rotation applied at render time, so Monday-first and Sunday-first are the same code.

Keyboard and ARIA

The popover is a dialog labelled by its purpose; each month is a grid whose cells are buttons under a roving tabindex, so exactly one day is in the tab order at a time. Arrow keys walk by day and by week, PageUp and PageDown change month, Home and End jump to the ends of the week, Enter selects, and Escape closes and hands focus back to the field. A blocked day stays reachable but marked unavailable, so the arrow keys never hit a hole in the grid.

Difficulty Advanced
Includes HTML + JS + CSS source, documentation, AI setup prompt, lifetime updates

Paid UI element

Purchase to unlock the code.

Buying Date Range Picker Calendar opens the HTML, CSS and JavaScript source, the full documentation, an AI setup prompt for your editor, and every update we ship to it. Standard license: unlimited personal and commercial projects.

£5 Standard license, unlimited projects

Browse UI elements

Documentation

What's Included

  • index.html: the demo page and the markup to copy

  • assets/style.css: component styles, plain CSS custom properties for colours

  • assets/script.js: readable, commented source with an onReady guard

  • Range selection in two clicks, with a live preview of the span between the start and wherever the pointer or the keyboard focus currently is

  • A staggered fill that runs from the start date to the end date when the range closes, and a damped pop on the endpoint that lands

  • Month navigation that slides the grid in the direction of travel and staggers the day cells from the leading edge

  • One month, or two side by side above 720px; below that it renders one, whatever data-months asks for

  • A presets column (today, last 7 days, last 30 days, this month), plus Clear and Apply

  • Minimum and maximum dates and individually blocked days, read live from data- attributes

  • role="dialog" popover over role="grid" months whose cells are gridcell buttons under a roving tabindex

  • One change event on the component root carrying start and end as ISO date strings

Quick Start

1. Add to your <head>:

Code snippet omitted: it ships with the download.

2. Add before the closing </body> tag:

Code snippet omitted: it ships with the download.

3. Copy the component markup from index.html into your page.

The script finds the component with [data-daterange] and everything inside it by data-daterange-* attribute, never by class name, so you can restyle or rename any class without touching the JavaScript. The parts it looks for:

Attribute What it marks Required
data-daterange the component root; carries every option below yes
data-daterange-field the button that opens the popover yes
data-daterange-value the element whose text becomes the selected range no
data-daterange-popover the panel, with role="dialog" and the hidden attribute yes
data-daterange-track the empty element the months are rendered into yes
data-daterange-title a live region announcing the visible months no
data-daterange-summary the "N days selected" line no
data-daterange-nav="prev" / "next" the month arrows no
data-daterange-preset="today|last7|last30|month" a quick-range button no
data-daterange-action="clear" / "apply" the footer buttons no
data-daterange-part any block that should fade up a frame after the panel no

Only the root, the field, the popover and the track are required; a picker with no presets, no footer and no summary is the same component with less markup.

The toolbar strip at the top of index.html and the .stage wrapper around the component are demo furniture. They are not part of what you bought: delete them and drop the .drp block into your own page.

Options

Every option is a data- attribute on the root, and every one is read live: change it with element.dataset.months = '1' and the calendar re-renders in place. Nothing needs a JavaScript API.

Attribute Values Default What it does
data-months 1, 2 1 How many months to show. Two only fit above 720px; below that one is rendered regardless
data-first-day 0-6 1 Which weekday the grid starts on. 1 is Monday, 0 is Sunday
data-min YYYY-MM-DD none Earliest selectable date. Earlier days are blocked and the back arrow stops at its month
data-max YYYY-MM-DD none Latest selectable date
data-disabled comma-separated YYYY-MM-DD none Individual days to block inside the allowed span
data-start YYYY-MM-DD none The range's start when the page loads
data-end YYYY-MM-DD none The range's end when the page loads
data-open true absent Opens the popover on load without moving focus

Code snippet omitted: it ships with the download.

Keyboard & Accessibility

Key What it does
Enter / Space on the field Opens the calendar and moves focus into the grid
ArrowDown on the field Opens the calendar
ArrowLeft / ArrowRight Moves the day focus back and forward one day
ArrowUp / ArrowDown Moves the day focus back and forward one week
Home / End Jumps to the first and last day of the focused week
PageUp / PageDown Moves the view back and forward one month
Enter / Space on a day Sets the start, or closes the range if a start is already set
Tab / Shift+Tab Cycles between the presets, the arrows, the focused day and the footer buttons
Escape Closes the calendar and returns focus to the field
  • Roles and states: the field is a button with aria-haspopup="dialog" and aria-expanded; the panel is role="dialog" with an aria-label; each month is a role="grid" labelled with its month and year, with role="row" rows, role="columnheader" weekday headers carrying full day names, and role="gridcell" buttons carrying aria-label (the full date), aria-selected on the two endpoints and aria-current="date" on today.
  • Roving tabindex: exactly one day cell is in the tab order at a time, so Tab never walks 42 buttons. The arrow keys move which one it is, and moving past the edge of the visible months changes the view and keeps focus on the day.
  • A blocked day carries aria-disabled="true" rather than the disabled attribute, so the arrow keys can still travel over it; it simply does not respond to Enter or a click, and its number is ruled through rather than faded to something unreadable.
  • The visible month names are the on-screen labels; the header also holds a visually hidden aria-live="polite" line, so a screen reader hears the new months after a jump.
  • Focus is trapped inside the popover while it is open, moved into the grid when it opens, and returned to the field when it closes. The page behind is not locked: a popover anchored to a field is not a modal, and locking the document under one is the wrong behaviour.
  • Clicking anywhere outside the popover closes it without moving focus.
  • prefers-reduced-motion: the same states, with no tweening. The panel appears in place, months change instantly and the range fill is simply there. Nothing is disabled and nothing is hidden.

Themes

Ships in two: light (the default) and dark. They are one design at two token values. Set the attribute on <body>, or on the component root itself when only this component should flip:

Code snippet omitted: it ships with the download.

Nothing else changes: same markup, same script, and nothing in assets/script.js ever reads the theme name. The demo's toggle and the ?variant=dark URL parameter only set that attribute.

Every colour, radius and shadow the component uses is a custom property in the body[data-variant="light"] and body[data-variant="dark"] blocks at the top of assets/style.css. To wear your own brand, re-value this short list in both blocks:

Property What it paints
--raised the field and the popover surface
--ground / --ground-2 the page behind, and the disabled Apply button
--ink / --ink-2 / --ink-3 day numbers, preset labels, weekday initials and meta
--line / --line-strong hairlines and the field border
--accent / --accent-ink the selected endpoints, the primary button and the focus ring
--accent-text the today mark and the primary button's hover
--range-bg / --range-ink the wash across the range and the numbers inside it
--day-hover a day cell under the pointer
--radius / --radius-sm the panel, and the controls and day cells
--day-size the width and height of one day cell (on .drp, not the theme block)

Keep --range-bg a solid hex rather than a translucent colour: the day numbers sit on top of them, and a translucent background makes the contrast unmeasurable for both an audit tool and a browser's own high-contrast pass.

How It Works

Opening in two beats. The panel animates first with a short power3.out on opacity, y and a small scale, and everything marked data-daterange-part fades up behind it on a 0.06s delay with a 0.035s stagger. The panel is animated on opacity, never autoAlpha: autoAlpha writes visibility: hidden on the same frame, and an element that is not visible yet cannot take focus, so the grid would never receive it. Presence is controlled by the hidden attribute instead, set in the close tween's onComplete so an invisible panel is never left covering the page.

The fill sweep. Each day cell is three layers: a wash, a cap, and the number inside the cap. Closing a range collects the cells between the endpoints in date order and runs one gsap.fromTo across their wash layers, scaling each from its left edge with a stagger sized to the length of the range. The number lives inside the cap rather than beside it so the accent background is a real ancestor of the text; a sibling layer is invisible to a contrast audit and to a browser's forced-colours mode.

Month navigation. A month change rebuilds the grid and then animates it: the track slides in from the direction of travel while the day cells fade up in a stagger that starts from the leading edge. Every one of those tweens ends in clearProps, because a cell left holding an inline transform mis-measures the next time the grid is rebuilt, and the panel's own transform is cleared on close so a second open cannot start from where the last close finished.

Dates without a parser. A date is a { y, m, d } record compared as one sortable integer. Date is used only through its UTC constructor, to ask what weekday a day falls on and how many days a month has. Nothing is built by parsing a string: new Date('2026-09-12') is UTC midnight, which is the 11th anywhere west of Greenwich, and that shifts a whole calendar by one column for a large part of the world. The first day of the week is a rotation applied at render time, so Monday-first and Sunday-first run the same code path.

Customisation

  • Cell size is --day-size on .drp (32px above 720px, 38px below). Everything in the grid derives from it.
  • The month and weekday names are the four arrays at the top of assets/script.js; replace them to ship the component in another language.
  • The presets are markup, not configuration: delete one, reorder them, or add your own by handling click on a new button and setting the range you want.
  • Easings and durations are the gsap.to / gsap.fromTo calls in assets/script.js; the sweep's stagger is capped so a long range still finishes quickly.
  • Colours live in assets/style.css as custom properties per theme.

Requirements

  • GSAP 3.12+ (core only, no plugins)
  • No build step, no framework

Worked examples, the events and programmatic API, and the class reference ship with the download, alongside the full source.

Your cart

Your cart is empty

The Vault £99

Everything in the catalogue, plus everything we release next.