Range Calendar
Presents a calendar view tailored for selecting date ranges.
Su | Mo | Tu | We | Th | Fr | Sa |
---|---|---|---|---|---|---|
26 | 27 | 28 | 29 | 30 | 31 | 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 |
<script lang="ts">
import { RangeCalendar } from "bits-ui";
import CaretLeft from "phosphor-svelte/lib/CaretLeft";
import CaretRight from "phosphor-svelte/lib/CaretRight";
import { cn } from "$lib/utils/index.js";
import type { ComponentProps } from "svelte";
let { value = $bindable() }: ComponentProps<typeof RangeCalendar.Root> =
$props();
</script>
<RangeCalendar.Root
class="rounded-15px border-dark-10 bg-background-alt shadow-card mt-6 border p-[22px]"
weekdayFormat="short"
fixedWeeks={true}
bind:value
>
{#snippet children({ months, weekdays })}
<RangeCalendar.Header class="flex items-center justify-between">
<RangeCalendar.PrevButton
class="rounded-9px bg-background-alt hover:bg-muted inline-flex size-10 items-center justify-center active:scale-[0.98]"
>
<CaretLeft class="size-6" />
</RangeCalendar.PrevButton>
<RangeCalendar.Heading class="text-[15px] font-medium" />
<RangeCalendar.NextButton
class="rounded-9px bg-background-alt hover:bg-muted inline-flex size-10 items-center justify-center active:scale-[0.98]"
>
<CaretRight class="size-6" />
</RangeCalendar.NextButton>
</RangeCalendar.Header>
<div
class="flex flex-col space-y-4 pt-4 sm:flex-row sm:space-x-4 sm:space-y-0"
>
{#each months as month}
<RangeCalendar.Grid
class="w-full border-collapse select-none space-y-1"
>
<RangeCalendar.GridHead>
<RangeCalendar.GridRow class="mb-1 flex w-full justify-between">
{#each weekdays as day}
<RangeCalendar.HeadCell
class="text-muted-foreground font-normal! w-10 rounded-md text-xs"
>
<div>{day.slice(0, 2)}</div>
</RangeCalendar.HeadCell>
{/each}
</RangeCalendar.GridRow>
</RangeCalendar.GridHead>
<RangeCalendar.GridBody>
{#each month.weeks as weekDates}
<RangeCalendar.GridRow class="flex w-full">
{#each weekDates as date}
<RangeCalendar.Cell
{date}
month={month.value}
class="p-0! relative m-0 size-10 text-center text-sm focus-within:z-20"
>
<RangeCalendar.Day
class={cn(
"rounded-9px bg-background text-foreground hover:border-foreground focus-visible:ring-foreground! data-selection-end:rounded-9px data-selection-start:rounded-9px data-highlighted:bg-muted data-selected:bg-muted data-selection-end:bg-foreground data-selection-start:bg-foreground data-disabled:text-foreground/30 data-selected:text-foreground data-selection-end:text-background data-selection-start:text-background data-unavailable:text-muted-foreground data-selected:[&:not([data-selection-start])]:[&:not([data-selection-end])]:focus-visible:border-foreground data-disabled:pointer-events-none data-outside-month:pointer-events-none data-highlighted:rounded-none data-selected:font-medium data-selection-end:font-medium data-selection-start:font-medium data-unavailable:line-through data-selection-start:focus-visible:ring-2 data-selection-start:focus-visible:ring-offset-2! data-selected:[&:not([data-selection-start])]:[&:not([data-selection-end])]:rounded-none data-selected:[&:not([data-selection-start])]:[&:not([data-selection-end])]:focus-visible:ring-0! data-selected:[&:not([data-selection-start])]:[&:not([data-selection-end])]:focus-visible:ring-offset-0! group relative inline-flex size-10 items-center justify-center overflow-visible whitespace-nowrap border border-transparent bg-transparent p-0 text-sm font-normal"
)}
>
<div
class="bg-foreground group-data-selected:bg-background group-data-today:block absolute top-[5px] hidden size-1 rounded-full"
></div>
{date.day}
</RangeCalendar.Day>
</RangeCalendar.Cell>
{/each}
</RangeCalendar.GridRow>
{/each}
</RangeCalendar.GridBody>
</RangeCalendar.Grid>
{/each}
</div>
{/snippet}
</RangeCalendar.Root>
@import url("https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600;1,700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap");
@import "tailwindcss";
@plugin "tailwindcss-animate";
@custom-variant dark (&:is(.dark *));
@font-face {
font-family: "Cal Sans";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("/CalSans-SemiBold.woff2") format("woff2");
}
:root {
/* Colors */
--background: hsl(0 0% 100%);
--background-alt: hsl(0 0% 100%);
--foreground: hsl(0 0% 9%);
--foreground-alt: hsl(0 0% 32%);
--muted: hsl(240 5% 96%);
--muted-foreground: hsla(0 0% 9% / 0.4);
--border: hsl(240 6% 10%);
--border-input: hsla(240 6% 10% / 0.17);
--border-input-hover: hsla(240 6% 10% / 0.4);
--border-card: hsla(240 6% 10% / 0.1);
--dark: hsl(240 6% 10%);
--dark-10: hsla(240 6% 10% / 0.1);
--dark-40: hsla(240 6% 10% / 0.4);
--dark-04: hsla(240 6% 10% / 0.04);
--accent: hsl(204 94% 94%);
--accent-foreground: hsl(204 80% 16%);
--destructive: hsl(347 77% 50%);
--tertiary: hsl(37.7 92.1% 50.2%);
--line: hsl(0 0% 100%);
/* black */
--contrast: hsl(0 0% 0%);
/* Shadows */
--shadow-mini: 0px 1px 0px 1px rgba(0, 0, 0, 0.04);
--shadow-mini-inset: 0px 1px 0px 0px rgba(0, 0, 0, 0.04) inset;
--shadow-popover: 0px 7px 12px 3px hsla(var(--dark-10));
--shadow-kbd: 0px 2px 0px 0px rgba(0, 0, 0, 0.07);
--shadow-btn: 0px 1px 0px 1px rgba(0, 0, 0, 0.03);
--shadow-card: 0px 2px 0px 1px rgba(0, 0, 0, 0.04);
--shadow-date-field-focus: 0px 0px 0px 3px rgba(24, 24, 27, 0.17);
}
.dark {
/* Colors */
--background: hsl(0 0% 5%);
--background-alt: hsl(0 0% 8%);
--foreground: hsl(0 0% 95%);
--foreground-alt: hsl(0 0% 70%);
--muted: hsl(240 4% 16%);
--muted-foreground: hsla(0 0% 100% / 0.4);
--border: hsl(0 0% 96%);
--border-input: hsla(0 0% 96% / 0.17);
--border-input-hover: hsla(0 0% 96% / 0.4);
--border-card: hsla(0 0% 96% / 0.1);
--dark: hsl(0 0% 96%);
--dark-40: hsl(0 0% 96% / 0.4);
--dark-10: hsl(0 0% 96% / 0.1);
--dark-04: hsl(0 0% 96% / 0.04);
--accent: hsl(204 90% 90%);
--accent-foreground: hsl(204 94% 94%);
--destructive: hsl(350 89% 60%);
--line: hsl(0 0% 9.02%);
--tertiary: hsl(61.3 100% 82.2%);
/* white */
--contrast: hsl(0 0% 100%);
/* Shadows */
--shadow-mini: 0px 1px 0px 1px rgba(0, 0, 0, 0.3);
--shadow-mini-inset: 0px 1px 0px 0px rgba(0, 0, 0, 0.5) inset;
--shadow-popover: 0px 7px 12px 3px hsla(0deg 0% 0% / 30%);
--shadow-kbd: 0px 2px 0px 0px rgba(255, 255, 255, 0.07);
--shadow-btn: 0px 1px 0px 1px rgba(0, 0, 0, 0.2);
--shadow-card: 0px 2px 0px 1px rgba(0, 0, 0, 0.4);
--shadow-date-field-focus: 0px 0px 0px 3px rgba(244, 244, 245, 0.1);
}
@theme inline {
--color-background: var(--background);
--color-background-alt: var(--background-alt);
--color-foreground: var(--foreground);
--color-foreground-alt: var(--foreground-alt);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-border: var(--border-card);
--color-border-input: var(--border-input);
--color-border-input-hover: var(--border-input-hover);
--color-border-card: var(--border-card);
--color-dark: var(--dark);
--color-dark-10: var(--dark-10);
--color-dark-40: var(--dark-40);
--color-dark-04: var(--dark-04);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-tertiary: var(--tertiary);
--color-line: var(--line);
--color-contrast: var(--contrast);
--shadow-mini: var(--shadow-mini);
--shadow-mini-inset: var(--shadow-mini-inset);
--shadow-popover: var(--shadow-popover);
--shadow-kbd: var(--shadow-kbd);
--shadow-btn: var(--shadow-btn);
--shadow-card: var(--shadow-card);
--shadow-date-field-focus: var(--shadow-date-field-focus);
--text-xxs: 10px;
--radius-card: 16px;
--radius-card-lg: 20px;
--radius-card-sm: 10px;
--radius-input: 9px;
--radius-button: 5px;
--radius-5px: 5px;
--radius-9px: 9px;
--radius-10px: 10px;
--radius-15px: 15px;
--spacing-input: 3rem;
--spacing-input-sm: 2.5rem;
--breakpoint-desktop: 1440px;
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
--animate-caret-blink: caret-blink 1s ease-out infinite;
--animate-scale-in: scale-in 0.2s ease;
--animate-scale-out: scale-out 0.15s ease;
--animate-fade-in: fade-in 0.2s ease;
--animate-fade-out: fade-out 0.15s ease;
--animate-enter-from-left: enter-from-left 0.2s ease;
--animate-enter-from-right: enter-from-right 0.2s ease;
--animate-exit-to-left: exit-to-left 0.2s ease;
--animate-exit-to-right: exit-to-right 0.2s ease;
--font-sans: "Inter", "sans-serif";
--font-mono: "Source Code Pro", "monospace";
--font-alt: "Courier", "sans-serif";
--font-display: "Cal Sans", "sans-serif";
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--bits-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--bits-accordion-content-height);
}
to {
height: 0;
}
}
@keyframes caret-blink {
0%,
70%,
100% {
opacity: 1;
}
20%,
50% {
opacity: 0;
}
}
@keyframes enter-from-right {
from {
opacity: 0;
transform: translateX(200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes enter-from-left {
from {
opacity: 0;
transform: translateX(-200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes exit-to-right {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(200px);
}
}
@keyframes exit-to-left {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(-200px);
}
}
@keyframes scale-in {
from {
opacity: 0;
transform: rotateX(-10deg) scale(0.9);
}
to {
opacity: 1;
transform: rotateX(0deg) scale(1);
}
}
@keyframes scale-out {
from {
opacity: 1;
transform: rotateX(0deg) scale(1);
}
to {
opacity: 0;
transform: rotateX(-10deg) scale(0.95);
}
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
}
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-border-card, currentColor);
}
* {
@apply border-border;
}
html {
-webkit-text-size-adjust: 100%;
font-variation-settings: normal;
scrollbar-color: var(--bg-muted);
}
body {
@apply bg-background text-foreground;
font-feature-settings:
"rlig" 1,
"calt" 1;
}
::selection {
background: #fdffa4;
color: black;
}
}
@layer components {
*:not(body):not(.focus-override) {
outline: none !important;
&:focus-visible {
@apply focus-visible:ring-foreground focus-visible:ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-1;
}
}
.link {
@apply hover:text-foreground/80 focus-visible:ring-foreground focus-visible:ring-offset-background rounded-xs focus-visible:outline-hidden inline-flex items-center gap-1 font-medium underline underline-offset-4 focus-visible:ring-2 focus-visible:ring-offset-2;
}
}
Heads up!
Before diving into this component, it's important to understand how dates/times work in Bits UI. Please read the Dates documentation to learn more!
Structure
<script lang="ts">
import { RangeCalendar } from "bits-ui";
</script>
<RangeCalendar.Root>
{#snippet children({ months, weekdays })}
<RangeCalendar.Header>
<RangeCalendar.PrevButton />
<RangeCalendar.Heading />
<RangeCalendar.NextButton />
</RangeCalendar.Header>
{#each months as month}
<RangeCalendar.Grid>
<RangeCalendar.GridHead>
<RangeCalendar.GridRow>
{#each weekdays as day}
<RangeCalendar.HeadCell>
{day}
</RangeCalendar.HeadCell>
{/each}
</RangeCalendar.GridRow>
</RangeCalendar.GridHead>
<RangeCalendar.GridBody>
{#each month.weeks as weekDates}
<RangeCalendar.GridRow>
{#each weekDates as date}
<RangeCalendar.Cell {date} month={month.value}>
<RangeCalendar.Day />
</RangeCalendar.Cell>
{/each}
</RangeCalendar.GridRow>
{/each}
</RangeCalendar.GridBody>
</RangeCalendar.Grid>
{/each}
{/snippet}
</RangeCalendar.Root>
API Reference
The root range calendar component which contains all other calendar components.
Property | Type | Description |
---|---|---|
value $bindable | DateRange | The selected date range. Default: undefined |
onValueChange | function | A function that is called when the selected date range changes. Default: undefined |
placeholder | DateValue | The placeholder date, which is used to determine what month to display when no date is selected. This updates as the user navigates the calendar, and can be used to programmatically control the calendar's view. Default: undefined |
onPlaceholderChange | function | A function that is called when the placeholder date changes. Default: undefined |
pagedNavigation | boolean | Whether or not to use paged navigation for the calendar. Paged navigation causes the previous and next buttons to navigate by the number of months displayed at once, rather than by one month. Default: false |
preventDeselect | boolean | Whether or not to prevent the user from deselecting a date without selecting another date first. Default: false |
weekdayFormat | enum | The format to use for the weekday strings provided via the Default: 'narrow' |
weekStartsOn | number | The day of the week to start the calendar on. 0 is Sunday, 1 is Monday, etc. Default: 0 |
calendarLabel | string | The accessible label for the calendar. Default: undefined |
fixedWeeks | boolean | Whether or not to always display 6 weeks in the calendar. Default: false |
isDateDisabled | function | A function that returns whether or not a date is disabled. Default: undefined |
isDateUnavailable | function | A function that returns whether or not a date is unavailable. Default: undefined |
maxValue | DateValue | The maximum date that can be selected. Default: undefined |
minValue | DateValue | The minimum date that can be selected. Default: undefined |
locale | string | The locale to use for formatting dates. Default: 'en' |
numberOfMonths | number | The number of months to display at once. Default: 1 |
disabled | boolean | Whether or not the accordion is disabled. Default: false |
readonly | boolean | Whether or not the calendar is readonly. Default: false |
disableDaysOutsideMonth | boolean | Whether or not to disable days outside the current month. Default: false |
onStartValueChange | function | A function that is called when the start date changes. Default: undefined |
onEndValueChange | function | A function that is called when the end date changes. Default: undefined |
ref $bindable | HTMLDivElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See Child Snippet docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-invalid | '' | Present on the root element when the calendar is invalid. |
data-disabled | '' | Present on the root element when the calendar is disabled. |
data-readonly | '' | Present on the root element when the calendar is readonly. |
data-calendar-root | '' | Present on the root element. |
The header of the calendar.
Property | Type | Description |
---|---|---|
ref $bindable | HTMLElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See Child Snippet docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-disabled | '' | Present on the header element when the calendar is disabled. |
data-readonly | '' | Present on the header element when the calendar is readonly. |
data-calendar-header | '' | Present on the header element. |
The heading of the calendar.
Property | Type | Description |
---|---|---|
ref $bindable | HTMLDivElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See Child Snippet docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-disabled | '' | Present on the heading element when the calendar is disabled. |
data-readonly | '' | Present on the heading element when the calendar is readonly. |
data-calendar-heading | '' | Present on the heading element. |
The next button of the calendar.
Property | Type | Description |
---|---|---|
ref $bindable | HTMLButtonElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See Child Snippet docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-disabled | '' | Present on the next button element when the calendar or this button is disabled. |
data-calendar-next-button | '' | Present on the next button element. |
The previous button of the calendar.
Property | Type | Description |
---|---|---|
ref $bindable | HTMLButtonElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See Child Snippet docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-disabled | '' | Present on the prev button element when the calendar or this button is disabled. |
data-calendar-prev-button | '' | Present on the prev button element. |
A cell in the calendar grid.
Property | Type | Description |
---|---|---|
date | DateValue | The date for the cell. Default: undefined |
month | DateValue | The current month the date is being displayed in. Default: undefined |
ref $bindable | HTMLTableCellElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See Child Snippet docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-disabled | '' | Present when the day is disabled. |
data-unavailable | '' | Present when the day is unavailable. |
data-today | '' | Present when the day is today. |
data-outside-month | '' | Present when the day is outside the current month. |
data-outside-visible-months | '' | Present when the day is outside the visible months. |
data-focused | '' | Present when the day is focused. |
data-selected | '' | Present when the day is selected. |
data-value | '' | The date in the format "YYYY-MM-DD". |
data-calendar-cell | '' | Present on the cell element. |
data-selection-start | '' | Present when the cell is the start of a selection. |
data-selection-end | '' | Present when the cell is the end of a selection. |
data-highlighted | '' | Present when the cell is highlighted within a range. |
A day in the calendar grid.
Property | Type | Description |
---|---|---|
ref $bindable | HTMLDivElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See Child Snippet docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-disabled | '' | Present when the day is disabled. |
data-unavailable | '' | Present when the day is unavailable. |
data-today | '' | Present when the day is today. |
data-outside-month | '' | Present when the day is outside the current month. |
data-outside-visible-months | '' | Present when the day is outside the visible months. |
data-focused | '' | Present when the day is focused. |
data-selected | '' | Present when the day is selected. |
data-value | '' | The date in the format "YYYY-MM-DD". |
data-calendar-cell | '' | Present on the cell element. |
data-selection-start | '' | Present when the cell is the start of a selection. |
data-selection-end | '' | Present when the cell is the end of a selection. |
data-highlighted | '' | Present when the cell is highlighted within a range. |
The grid of dates in the calendar, typically representing a month.
Property | Type | Description |
---|---|---|
ref $bindable | HTMLTableElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See Child Snippet docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-disabled | '' | Present on the grid element when the calendar is disabled. |
data-readonly | '' | Present on the grid element when the calendar is readonly. |
data-calendar-grid | '' | Present on the grid element. |
The body of the grid of dates in the calendar.
Property | Type | Description |
---|---|---|
ref $bindable | HTMLTableSectionElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See Child Snippet docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-disabled | '' | Present on the grid element when the calendar is disabled. |
data-readonly | '' | Present on the grid element when the calendar is readonly. |
data-calendar-grid-body | '' | Present on the grid body element. |
The head of the grid of dates in the calendar.
Property | Type | Description |
---|---|---|
ref $bindable | HTMLTableSectionElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See Child Snippet docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-disabled | '' | Present on the grid head element when the calendar is disabled. |
data-readonly | '' | Present on the grid head element when the calendar is readonly. |
data-calendar-grid-head | '' | Present on the grid head element. |
A row in the grid of dates in the calendar.
Property | Type | Description |
---|---|---|
ref $bindable | HTMLTableRowElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See Child Snippet docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-disabled | '' | Present on the grid row element when the calendar is disabled. |
data-readonly | '' | Present on the grid row element when the calendar is readonly. |
data-calendar-grid-row | '' | Present on the grid row element. |
A cell in the head of the grid of dates in the calendar.
Property | Type | Description |
---|---|---|
ref $bindable | HTMLTableCellElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See Child Snippet docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-disabled | '' | Present on the head cell element when the calendar is disabled. |
data-readonly | '' | Present on the head cell element when the calendar is readonly. |
data-calendar-head-cell | '' | Present on the head cell element. |