# Date Range Picker Calendar

> 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.

Canonical: https://gsapvault.com/ui-elements/date-range-picker-calendar
Live demo: https://gsapvault.com/demos/date-range-picker-calendar/index.html

| Property | Value |
|----------|-------|
| Type | ui-element |
| Tier | paid |
| Price | £5 |
| Difficulty | advanced |
| Plugins | Core GSAP only |
| Techniques | stagger, keyboard-navigation, micro-interaction, dialog-animation |
| Uses Lenis | No |

## Overview

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.

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.

## Features

- 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

## 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

## Vibe-Code Ready Setup

This UI element includes `START-HERE-AI.md`, a product-specific copy-paste setup prompt for Cursor, Claude Code, ChatGPT, GitHub Copilot, Windsurf, and other coding assistants. It tells the assistant to inspect the existing stack, integrate the supplied files, preserve the design, scope selectors, retain accessibility and responsive behaviour, add framework-appropriate GSAP cleanup, and report what it tested.

[How AI-assisted setup works](https://gsapvault.com/vibe-coding)

## How It Works

### 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.

## Documentation

How this UI element works and how it goes into a page. The reference you use once you own the files (worked examples, events, the programmatic API, the class list) ships with the download.

### 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

<!-- Version convention (enforced by `bun run update-cdns`):
     three-part "3.15.0" = a pin, rewritten on every bump.
     two-part  "3.12+"   = a compatibility floor, never rewritten. -->

## What You Get

- `index.html`: working demo page
- `assets/script.js`: commented, readable source
- `assets/style.css`: effect styles
- `README.md`: full documentation with examples and framework integration notes
- `START-HERE-AI.md`: product-specific copy-paste prompt for AI-assisted setup
- `LICENSE.txt`: standard license terms
- Lifetime updates: re-download anytime from your library

## Get the Code

This is a premium UI element. The standard license costs £5 one-time and covers unlimited personal and commercial projects with no attribution required. The only restrictions: no redistribution of the code itself and no competing effect libraries.

- [Buy Date Range Picker Calendar](https://gsapvault.com/ui-elements/date-range-picker-calendar)
- [The Vault (£99 one-time, best value): everything in the catalogue, including future releases](https://gsapvault.com/effects)

---

From [GSAP Vault](https://gsapvault.com): production-ready GSAP animation effects. Full catalog for agents: https://gsapvault.com/llms-full.txt
