# Range Calendar

A date-range picker that is also a heatmap. Each day is a rounded square shaded by its value, from fewer to more. Click a day to start a range and click another to end it; the days between get a soft band, and the pill above shows the dates and the length. While you pick, hovering a day previews the range. Presets set the range in one click, and the switch turns on a comparison with the previous period. Days after `today` and days without data show a dashed outline and cannot be picked.

<demo-range-calendar>



</demo-range-calendar>

## Installation

```bash
npx shadcn-vue@latest add "https://nxui.geoql.in/r/range-calendar.json"
```

## Usage

```vue
<script setup lang="ts">
  import { ref } from 'vue';
  import RangeCalendar from '~/components/ui/range-calendar/RangeCalendar.vue';
  import type { RangeCalendarDay } from '~/components/ui/range-calendar/types';

  const days: RangeCalendarDay[] = [
    { date: '2026-09-01', value: 142 },
    { date: '2026-09-02', value: 61 },
  ];

  const start = ref('2026-09-04');
  const end = ref('2026-09-25');
  const preset = ref('1M');
  const compare = ref(true);
</script>

<template>
  <RangeCalendar
    v-model:start="start"
    v-model:end="end"
    v-model:preset="preset"
    v-model:compare="compare"
    today="2026-09-28"
    :days="days"
    title="Time Range"
    subtitle="Line 3 · all cells"
  />
</template>
```

Dates are plain `YYYY-MM-DD` strings. The component does calendar math on day numbers in UTC, so the time zone never moves a day. Pass `today` yourself instead of reading the clock, so the server and the browser render the same month.

Picking days by hand clears `preset`. Each preset ends on `today`; presets with `hours` cover `today` only and show the hours in the pill.

## Props

<table>
<thead>
  <tr>
    <th>
      Prop
    </th>
    
    <th>
      Type
    </th>
    
    <th>
      Default
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        today
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      The current day, <code>
        YYYY-MM-DD
      </code>
      
      . Later days are disabled; presets end here.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        start
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        ''
      </code>
    </td>
    
    <td>
      <code>
        v-model
      </code>
      
      . First day of the range, <code>
        YYYY-MM-DD
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        end
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        ''
      </code>
    </td>
    
    <td>
      <code>
        v-model
      </code>
      
      . Last day of the range, <code>
        YYYY-MM-DD
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        preset
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        ''
      </code>
    </td>
    
    <td>
      <code>
        v-model
      </code>
      
      . Value of the active preset, or <code>
        ''
      </code>
      
       for a custom range.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        compare
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        false
      </code>
    </td>
    
    <td>
      <code>
        v-model
      </code>
      
      . State of the compare switch.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        month
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      month of <code>
        today
      </code>
    </td>
    
    <td>
      Month to show, <code>
        YYYY-MM
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        days
      </code>
    </td>
    
    <td>
      <code>
        RangeCalendarDay[]
      </code>
    </td>
    
    <td>
      <code>
        []
      </code>
    </td>
    
    <td>
      <code>
        { date, value }
      </code>
      
       per day. Shades the heatmap. Days without an entry are off.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        title
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        'Time Range'
      </code>
    </td>
    
    <td>
      Card heading.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        subtitle
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        ''
      </code>
    </td>
    
    <td>
      Line under the heading.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        presets
      </code>
    </td>
    
    <td>
      <code>
        RangeCalendarPreset[]
      </code>
    </td>
    
    <td>
      <code>
        12h 24h 1W 1M 3M
      </code>
    </td>
    
    <td>
      <code>
        { value, label, days, hours? }
      </code>
      
       buttons above the grid.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        unit
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        'stops'
      </code>
    </td>
    
    <td>
      Noun for the value, used in the legend and the day labels.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        legend-low
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        'fewer <unit>'
      </code>
    </td>
    
    <td>
      Left legend label.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        legend-high
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        'more'
      </code>
    </td>
    
    <td>
      Right legend label.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        compare-title
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        'Compare'
      </code>
    </td>
    
    <td>
      Label next to the switch.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        compare-label
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        'previous <N> days'
      </code>
    </td>
    
    <td>
      Text under the label. <code>
        N
      </code>
      
       is the preset length, or the range length.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        locale
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        'en-US'
      </code>
    </td>
    
    <td>
      Locale for month and weekday names.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        class
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        ''
      </code>
    </td>
    
    <td>
      Extra classes for the root element.
    </td>
  </tr>
</tbody>
</table>

## Accessibility

- The month is a `role="grid"` with weekday column headers. Each day is a button with a full label, for example "Friday, September 4, 2026, 33 stops".
- Arrow keys move between days, Home and End jump to the ends of the week, Enter or Space picks a day, and Escape cancels a half-picked range. Only one day is in the tab order.
- Days in the range set `aria-selected`. Disabled days set `aria-disabled` and stay focusable so the keyboard can pass over them. Today sets `aria-current="date"`.
- The range pill is announced through `aria-live`. Presets are toggle buttons with `aria-pressed`.
- The compare control is a `role="switch"` button with `aria-checked`.
- Transitions turn off with `prefers-reduced-motion`.
