| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
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 | 29 | 30 | 1 | 2 | 3 | 4 |
<script lang="ts">
import { getLocalTimeZone, today } from "@internationalized/date";
import { RangeCalendar } from "$lib/components/ui/range-calendar/index.js";
const start = today(getLocalTimeZone());
const end = start.add({ days: 7 });
let value = $state({
start,
end
});
</script>
<RangeCalendar bind:value class="rounded-md border" /> About
The <RangeCalendar /> component is built on top of the Bits Range Calendar component, which uses the @internationalized/date package to handle dates.
Blocks
Installation
pnpm dlx shadcn-svelte@latest add range-calendar Install bits-ui and @internationalized/date:
pnpm add bits-ui @internationalized/date -D Copy and paste the following code into your project.
import Root from './range-calendar.svelte';
import Cell from './range-calendar-cell.svelte';
import Day from './range-calendar-day.svelte';
import Grid from './range-calendar-grid.svelte';
import Header from './range-calendar-header.svelte';
import Months from './range-calendar-months.svelte';
import GridRow from './range-calendar-grid-row.svelte';
import Heading from './range-calendar-heading.svelte';
import HeadCell from './range-calendar-head-cell.svelte';
import NextButton from './range-calendar-next-button.svelte';
import PrevButton from './range-calendar-prev-button.svelte';
import MonthSelect from './range-calendar-month-select.svelte';
import YearSelect from './range-calendar-year-select.svelte';
import Caption from './range-calendar-caption.svelte';
import Nav from './range-calendar-nav.svelte';
import Month from './range-calendar-month.svelte';
import GridBody from './range-calendar-grid-body.svelte';
import GridHead from './range-calendar-grid-head.svelte';
export {
Day,
Cell,
Grid,
Header,
Months,
GridRow,
Heading,
GridBody,
GridHead,
HeadCell,
NextButton,
PrevButton,
MonthSelect,
YearSelect,
Caption,
Nav,
Month,
//
Root as RangeCalendar
};
<script lang="ts">
import type { ComponentProps } from 'svelte';
import type RangeCalendar from './range-calendar.svelte';
import RangeCalendarMonthSelect from './range-calendar-month-select.svelte';
import RangeCalendarYearSelect from './range-calendar-year-select.svelte';
import { DateFormatter, getLocalTimeZone, type DateValue } from '@internationalized/date';
let {
captionLayout,
months,
monthFormat,
years,
yearFormat,
month,
locale,
placeholder = $bindable(),
monthIndex = 0
}: {
captionLayout: ComponentProps<typeof RangeCalendar>['captionLayout'];
months: ComponentProps<typeof RangeCalendarMonthSelect>['months'];
monthFormat: ComponentProps<typeof RangeCalendarMonthSelect>['monthFormat'];
years: ComponentProps<typeof RangeCalendarYearSelect>['years'];
yearFormat: ComponentProps<typeof RangeCalendarYearSelect>['yearFormat'];
month: DateValue;
placeholder: DateValue | undefined;
locale: string;
monthIndex: number;
} = $props();
function formatYear(date: DateValue) {
const dateObj = date.toDate(getLocalTimeZone());
if (typeof yearFormat === 'function') return yearFormat(dateObj.getFullYear());
return new DateFormatter(locale, { year: yearFormat }).format(dateObj);
}
function formatMonth(date: DateValue) {
const dateObj = date.toDate(getLocalTimeZone());
if (typeof monthFormat === 'function') return monthFormat(dateObj.getMonth() + 1);
return new DateFormatter(locale, { month: monthFormat }).format(dateObj);
}
</script>
{#snippet MonthSelect()}
<RangeCalendarMonthSelect
class="font-mono text-xs font-semibold tracking-[0.14em] uppercase"
{months}
{monthFormat}
value={month.month}
onchange={(e) => {
if (!placeholder) return;
const v = Number.parseInt(e.currentTarget.value);
const newPlaceholder = placeholder.set({ month: v });
placeholder = newPlaceholder.subtract({ months: monthIndex });
}}
/>
{/snippet}
{#snippet YearSelect()}
<RangeCalendarYearSelect
class="font-mono text-xs font-semibold tracking-[0.14em] uppercase"
{years}
{yearFormat}
value={month.year}
/>
{/snippet}
{#if captionLayout === 'dropdown'}
{@render MonthSelect()}
{@render YearSelect()}
{:else if captionLayout === 'dropdown-months'}
{@render MonthSelect()}
{#if placeholder}
<span class="font-mono text-xs font-semibold tracking-[0.14em] text-zinc-100 uppercase"
>{formatYear(placeholder)}</span
>
{/if}
{:else if captionLayout === 'dropdown-years'}
{#if placeholder}
<span class="font-mono text-xs font-semibold tracking-[0.14em] text-zinc-100 uppercase"
>{formatMonth(placeholder)}</span
>
{/if}
{@render YearSelect()}
{:else}
<span class="font-mono text-xs font-semibold tracking-[0.14em] text-zinc-100 uppercase"
>{formatMonth(month)} {formatYear(month)}</span
>
{/if}
<script lang="ts">
import { RangeCalendar as RangeCalendarPrimitive } from 'bits-ui';
import { cn } from '$UTILS$.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: RangeCalendarPrimitive.CellProps = $props();
</script>
<RangeCalendarPrimitive.Cell
bind:ref
class={cn(
'relative size-(--cell-size) border-r border-b border-zinc-900 p-0 text-center text-sm focus-within:z-20 data-[range-middle]:rounded-e-(--cell-radius) [&:has([data-range-middle])]:bg-[#b9d765]/20 [&:has([data-selected])]:bg-[#b9d765]/15 [&:first-child[data-selected]_[data-bits-day]]:rounded-s-(--cell-radius) [&:has([data-range-end])]:rounded-e-(--cell-radius) [&:has([data-range-middle])]:rounded-none first:[&:has([data-range-middle])]:rounded-s-(--cell-radius) last:[&:has([data-range-middle])]:rounded-e-(--cell-radius) [&:has([data-range-start])]:rounded-s-(--cell-radius) [&:last-child[data-selected]_[data-bits-day]]:rounded-e-(--cell-radius)',
className
)}
{...restProps}
/>
<script lang="ts">
import { RangeCalendar as RangeCalendarPrimitive } from 'bits-ui';
import { cn } from '$UTILS$.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: RangeCalendarPrimitive.DayProps = $props();
</script>
<RangeCalendarPrimitive.Day
bind:ref
class={cn(
'flex size-(--cell-size) flex-col items-center justify-center gap-1 rounded-(--cell-radius) border border-transparent p-0 font-mono text-xs leading-none font-medium whitespace-nowrap text-zinc-300 select-none',
'not-data-selected:hover:border-zinc-700 not-data-selected:hover:bg-zinc-950 not-data-selected:hover:text-white',
'[&[data-today]:not([data-selected])]:border-[#d0e891]/60 [&[data-today]:not([data-selected])]:bg-zinc-950 [&[data-today]:not([data-selected])]:text-[#d0e891] [&[data-today][data-disabled]]:text-zinc-500 data-[range-middle]:rounded-none',
// range Start
'data-[range-start]:border-[#b9d765] data-[range-start]:bg-[#b9d765] data-[range-start]:text-[#101207] data-[range-start]:hover:text-[#101207]',
// range End
'data-[range-end]:border-[#b9d765] data-[range-end]:bg-[#b9d765] data-[range-end]:text-[#101207] data-[range-end]:hover:text-[#101207]',
// Outside months
'[&[data-outside-month]:not([data-selected])]:text-zinc-700 [&[data-outside-month]:not([data-selected])]:hover:text-zinc-400',
// Disabled
'data-[disabled]:pointer-events-none data-[disabled]:text-zinc-600 data-[disabled]:opacity-50',
// Unavailable
'data-[unavailable]:line-through',
'data-[range-middle]:bg-[#b9d765]/20 data-[range-middle]:text-zinc-100 dark:data-[range-middle]:hover:bg-[#b9d765]/20',
// focus
'focus:relative focus:border-zinc-300 focus:ring-2 focus:ring-zinc-300/30',
// inner spans
'[&>span]:text-xs [&>span]:opacity-70',
className
)}
{...restProps}
/>
<script lang="ts">
import { RangeCalendar as RangeCalendarPrimitive } from 'bits-ui';
import { cn } from '$UTILS$.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: RangeCalendarPrimitive.GridBodyProps = $props();
</script>
<RangeCalendarPrimitive.GridBody bind:ref class={cn('bg-background', className)} {...restProps} />
<script lang="ts">
import { RangeCalendar as RangeCalendarPrimitive } from 'bits-ui';
import { cn } from '$UTILS$.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: RangeCalendarPrimitive.GridHeadProps = $props();
</script>
<RangeCalendarPrimitive.GridHead bind:ref class={cn('bg-zinc-950/70', className)} {...restProps} />
<script lang="ts">
import { RangeCalendar as RangeCalendarPrimitive } from 'bits-ui';
import { cn } from '$UTILS$.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: RangeCalendarPrimitive.GridRowProps = $props();
</script>
<RangeCalendarPrimitive.GridRow
bind:ref
class={cn('flex divide-x divide-zinc-900', className)}
{...restProps}
/>
<script lang="ts">
import { RangeCalendar as RangeCalendarPrimitive } from 'bits-ui';
import { cn } from '$UTILS$.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: RangeCalendarPrimitive.GridProps = $props();
</script>
<RangeCalendarPrimitive.Grid
bind:ref
class={cn('flex w-full border-collapse flex-col border border-zinc-900 bg-background', className)}
{...restProps}
/>
<script lang="ts">
import { RangeCalendar as RangeCalendarPrimitive } from 'bits-ui';
import { cn } from '$UTILS$.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: RangeCalendarPrimitive.HeadCellProps = $props();
</script>
<RangeCalendarPrimitive.HeadCell
bind:ref
class={cn(
'w-(--cell-size) py-1 font-mono text-[0.7rem] font-semibold tracking-[0.14em] text-zinc-500 uppercase',
className
)}
{...restProps}
/>
<script lang="ts">
import { RangeCalendar as RangeCalendarPrimitive } from 'bits-ui';
import { cn } from '$UTILS$.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: RangeCalendarPrimitive.HeaderProps = $props();
</script>
<RangeCalendarPrimitive.Header
bind:ref
class={cn(
'flex h-(--cell-size) w-full items-center justify-center gap-1.5 border-b border-zinc-900 text-sm font-medium',
className
)}
{...restProps}
/>
<script lang="ts">
import { RangeCalendar as RangeCalendarPrimitive } from 'bits-ui';
import { cn } from '$UTILS$.js';
let {
ref = $bindable(null),
class: className,
...restProps
}: RangeCalendarPrimitive.HeadingProps = $props();
</script>
<RangeCalendarPrimitive.Heading
bind:ref
class={cn(
'px-(--cell-size) font-mono text-xs font-semibold tracking-[0.14em] text-zinc-100 uppercase',
className
)}
{...restProps}
/>
<script lang="ts">
import { RangeCalendar as RangeCalendarPrimitive } from 'bits-ui';
import { cn, type WithoutChildrenOrChild } from '$UTILS$.js';
import CaretDownIcon from 'phosphor-svelte/lib/CaretDown';
let {
ref = $bindable(null),
class: className,
value,
onchange,
...restProps
}: WithoutChildrenOrChild<RangeCalendarPrimitive.MonthSelectProps> = $props();
</script>
<span
class={cn(
'relative flex border border-zinc-800 bg-zinc-950 text-zinc-100 has-focus:border-zinc-300 has-focus:ring-2 has-focus:ring-zinc-300/30',
className
)}
>
<RangeCalendarPrimitive.MonthSelect bind:ref class="absolute inset-0 opacity-0" {...restProps}>
{#snippet child({ props, monthItems, selectedMonthItem })}
<select {...props} {value} {onchange}>
{#each monthItems as monthItem (monthItem.value)}
<option
value={monthItem.value}
selected={value !== undefined
? monthItem.value === value
: monthItem.value === selectedMonthItem.value}
>
{monthItem.label}
</option>
{/each}
</select>
<span
class="flex h-(--cell-size) items-center gap-1 ps-2 pe-1 text-sm font-medium text-zinc-100 select-none [&>svg]:size-3.5 [&>svg]:text-zinc-500"
aria-hidden="true"
>
{monthItems.find((item) => item.value === value)?.label || selectedMonthItem.label}
<CaretDownIcon class={cn('size-4', className)} />
</span>
{/snippet}
</RangeCalendarPrimitive.MonthSelect>
</span>
<script lang="ts">
import { type WithElementRef, cn } from '$UTILS$.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
</script>
<div {...restProps} bind:this={ref} class={cn('flex flex-col gap-3', className)}>
{@render children?.()}
</div>
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { cn, type WithElementRef } from '$UTILS$.js';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
class={cn('relative flex flex-col gap-4 text-zinc-100 md:flex-row', className)}
{...restProps}
>
{@render children?.()}
</div>
<script lang="ts">
import { cn, type WithElementRef } from '$UTILS$.js';
import type { HTMLAttributes } from 'svelte/elements';
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLElement>> = $props();
</script>
<nav
{...restProps}
bind:this={ref}
class={cn(
'absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1 text-zinc-300',
className
)}
>
{@render children?.()}
</nav>
<script lang="ts">
import { RangeCalendar as RangeCalendarPrimitive } from 'bits-ui';
import CaretRightIcon from 'phosphor-svelte/lib/CaretRight';
import { buttonVariants, type ButtonVariant } from '$UI$/button/index.js';
import { cn } from '$UTILS$.js';
let {
ref = $bindable(null),
class: className,
children,
variant = 'ghost',
...restProps
}: RangeCalendarPrimitive.NextButtonProps & {
variant?: ButtonVariant;
} = $props();
</script>
{#snippet Fallback()}
<CaretRightIcon class={cn('size-4', className)} />
{/snippet}
<RangeCalendarPrimitive.NextButton
bind:ref
class={cn(
buttonVariants({ variant }),
'size-(--cell-size) rounded-full border border-zinc-800 bg-transparent p-0 text-zinc-300 select-none hover:border-[#d0e891] hover:bg-[#b9d765] hover:text-[#101207] disabled:opacity-50 rtl:rotate-180',
className
)}
{...restProps}
>
{#if children}
{@render children?.()}
{:else}
{@render Fallback()}
{/if}
</RangeCalendarPrimitive.NextButton>
<script lang="ts">
import { RangeCalendar as RangeCalendarPrimitive } from 'bits-ui';
import CaretLeftIcon from 'phosphor-svelte/lib/CaretLeft';
import { buttonVariants, type ButtonVariant } from '$UI$/button/index.js';
import { cn } from '$UTILS$.js';
let {
ref = $bindable(null),
class: className,
children,
variant = 'ghost',
...restProps
}: RangeCalendarPrimitive.PrevButtonProps & {
variant?: ButtonVariant;
} = $props();
</script>
{#snippet Fallback()}
<CaretLeftIcon class={cn('size-4', className)} />
{/snippet}
<RangeCalendarPrimitive.PrevButton
bind:ref
class={cn(
buttonVariants({ variant }),
'size-(--cell-size) rounded-full border border-zinc-800 bg-transparent p-0 text-zinc-300 select-none hover:border-[#d0e891] hover:bg-[#b9d765] hover:text-[#101207] disabled:opacity-50 rtl:rotate-180',
className
)}
{...restProps}
>
{#if children}
{@render children?.()}
{:else}
{@render Fallback()}
{/if}
</RangeCalendarPrimitive.PrevButton>
<script lang="ts">
import { RangeCalendar as RangeCalendarPrimitive } from 'bits-ui';
import { cn, type WithoutChildrenOrChild } from '$UTILS$.js';
import CaretDownIcon from 'phosphor-svelte/lib/CaretDown';
let {
ref = $bindable(null),
class: className,
value,
...restProps
}: WithoutChildrenOrChild<RangeCalendarPrimitive.YearSelectProps> = $props();
</script>
<span
class={cn(
'relative flex border border-zinc-800 bg-zinc-950 text-zinc-100 has-focus:border-zinc-300 has-focus:ring-2 has-focus:ring-zinc-300/30',
className
)}
>
<RangeCalendarPrimitive.YearSelect bind:ref class="absolute inset-0 opacity-0" {...restProps}>
{#snippet child({ props, yearItems, selectedYearItem })}
<select {...props} {value}>
{#each yearItems as yearItem (yearItem.value)}
<option
value={yearItem.value}
selected={value !== undefined
? yearItem.value === value
: yearItem.value === selectedYearItem.value}
>
{yearItem.label}
</option>
{/each}
</select>
<span
class="flex h-(--cell-size) items-center gap-1 ps-2 pe-1 text-sm font-medium text-zinc-100 select-none [&>svg]:size-3.5 [&>svg]:text-zinc-500"
aria-hidden="true"
>
{yearItems.find((item) => item.value === value)?.label || selectedYearItem.label}
<CaretDownIcon class={cn('size-4', className)} />
</span>
{/snippet}
</RangeCalendarPrimitive.YearSelect>
</span>
<script lang="ts">
import { RangeCalendar as RangeCalendarPrimitive } from 'bits-ui';
import * as RangeCalendar from './index.js';
import { cn, type WithoutChildrenOrChild } from '$UTILS$.js';
import type { ButtonVariant } from '$UI$/button/index.js';
import type { Snippet } from 'svelte';
import { isEqualMonth, type DateValue } from '@internationalized/date';
let {
ref = $bindable(null),
value = $bindable(),
placeholder = $bindable(),
weekdayFormat = 'short',
class: className,
buttonVariant = 'ghost',
captionLayout = 'label',
locale = 'en-US',
months: monthsProp,
years,
monthFormat: monthFormatProp,
yearFormat = 'numeric',
day,
disableDaysOutsideMonth = false,
...restProps
}: WithoutChildrenOrChild<RangeCalendarPrimitive.RootProps> & {
buttonVariant?: ButtonVariant;
captionLayout?: 'dropdown' | 'dropdown-months' | 'dropdown-years' | 'label';
months?: RangeCalendarPrimitive.MonthSelectProps['months'];
years?: RangeCalendarPrimitive.YearSelectProps['years'];
monthFormat?: RangeCalendarPrimitive.MonthSelectProps['monthFormat'];
yearFormat?: RangeCalendarPrimitive.YearSelectProps['yearFormat'];
day?: Snippet<[{ day: DateValue; outsideMonth: boolean }]>;
} = $props();
const monthFormat = $derived.by(() => {
if (monthFormatProp) return monthFormatProp;
if (captionLayout.startsWith('dropdown')) return 'short';
return 'long';
});
</script>
<RangeCalendarPrimitive.Root
bind:ref
bind:value
bind:placeholder
{weekdayFormat}
{disableDaysOutsideMonth}
class={cn(
'group/calendar border border-zinc-800 bg-background p-3 text-zinc-100 [--cell-radius:0] [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent',
className
)}
{locale}
{monthFormat}
{yearFormat}
{...restProps}
>
{#snippet children({ months, weekdays })}
<RangeCalendar.Months>
<RangeCalendar.Nav>
<RangeCalendar.PrevButton variant={buttonVariant} />
<RangeCalendar.NextButton variant={buttonVariant} />
</RangeCalendar.Nav>
{#each months as month, monthIndex (month)}
<RangeCalendar.Month>
<RangeCalendar.Header>
<RangeCalendar.Caption
{captionLayout}
months={monthsProp}
{monthFormat}
{years}
{yearFormat}
month={month.value}
bind:placeholder
{locale}
{monthIndex}
/>
</RangeCalendar.Header>
<RangeCalendar.Grid>
<RangeCalendar.GridHead>
<RangeCalendar.GridRow class="select-none border-b border-zinc-900">
{#each weekdays as weekday, i (i)}
<RangeCalendar.HeadCell>
{weekday.slice(0, 2)}
</RangeCalendar.HeadCell>
{/each}
</RangeCalendar.GridRow>
</RangeCalendar.GridHead>
<RangeCalendar.GridBody>
{#each month.weeks as weekDates (weekDates)}
<RangeCalendar.GridRow class="w-full">
{#each weekDates as date (date)}
<RangeCalendar.Cell {date} month={month.value}>
{#if day}
{@render day({
day: date,
outsideMonth: !isEqualMonth(date, month.value)
})}
{:else}
<RangeCalendar.Day />
{/if}
</RangeCalendar.Cell>
{/each}
</RangeCalendar.GridRow>
{/each}
</RangeCalendar.GridBody>
</RangeCalendar.Grid>
</RangeCalendar.Month>
{/each}
</RangeCalendar.Months>
{/snippet}
</RangeCalendarPrimitive.Root>