Range Calendar
range-calendaruiInline from→to calendar grid. Click two dates; the range fills between. Plain-Date value API with min/max, disabled-date support, fixed weeks, and configurable week start.
When to use which
Decision GuideCalendar is the always-visible month grid. Date Picker is the form field with popover. Range Calendar manages multi-month spans, and Event Calendar is the multi-view scheduling engine.
| Component | Role | Use when |
|---|---|---|
| Calendar Embedded dashboard views or scheduling pages where the month grid stays continuously visible. | Grid Primitive | |
| RangeCalendarcurrent Contiguous multi-month booking ranges, check-in / check-out spans, and date windows. | Range Primitive | |
| DatePicker Forms, filter bars, and dialogs where space is tight and the calendar opens inside a popover. | Form Control | |
| EventCalendar Full calendar interfaces with month/week/day views, timed hourly intervals, and overlap collision lanes. | Scheduling Engine |
Installation
$pnpm dlx shadcn-svelte@latest add https://uipkge.dev/r/svelte/range-calendar.json$npx shadcn-svelte@latest add https://uipkge.dev/r/svelte/range-calendar.json$yarn dlx shadcn-svelte@latest add https://uipkge.dev/r/svelte/range-calendar.json$bunx shadcn-svelte@latest add https://uipkge.dev/r/svelte/range-calendar.jsonhttps://uipkge.dev/r/svelte/range-calendar.jsonInstalls to:src/lib/components/ui/range-calendar/Examples
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
valueThe controlled range. Bindable. When both `value` and `selected` are set, `value` wins. | DateRange | undefined | — | optional |
defaultValueInitial range for uncontrolled use. | DateRange | undefined | — | optional |
onValueChangeCalled with the new range after every commit. | (value: DateRange) => void | — | optional |
selectedReact DayPicker-parity alias for `value`. Accepts `{ from, to }` and converts to internal `{ start, end }`. Bindable. Stays in sync with `value`: user selection updates both, and external updates to either sync the other. | ReactDateRange | undefined | — | optional |
onSelectReact-parity alias for `onValueChange`. Fires with `{ from, to }` alongside `onValueChange`. | (range: ReactDateRange | undefined) => void | — | optional |
minValueEarliest selectable date. | Date | — | optional |
maxValueLatest selectable date. | Date | — | optional |
disabledWhen `true`, prevents the user from interacting with the calendar. | boolean | false | optional |
readonlyWhen `true`, dates cannot be selected but the calendar stays focusable. | boolean | false | optional |
fixedWeeksAlways render 6 week rows so the height never shifts month-to-month. | boolean | false | optional |
weekStartsOnFirst day of the week: 0 = Sunday … 6 = Saturday. | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 0 | optional |
numberOfMonthsHow many months to render side by side. | number | 1 | optional |
localeBCP 47 locale for month and weekday names. Defaults to the runtime locale. | string | — | optional |
defaultPlaceholderInitial visible month. Defaults to the start of `value`/`defaultValue`, else today. | Date | — | optional |
isDateDisabledExtra predicate to disable individual dates. | (date: Date) => boolean | — | optional |
isDateUnavailableExtra predicate to mark individual dates unavailable. | (date: Date) => boolean | — | optional |
children | Snippet<[{ months: CalendarMonth[]; weekDays: string[] }]> | — | optional |
ref | HTMLDivElement | null | — | optional |
npm dependencies
Includes
Used by
Files installed (13)
src/lib/components/ui/range-calendar/RangeCalendar.svelte18.7 kB
<script lang="ts" module> import type { Snippet } from 'svelte' import type { HTMLAttributes } from 'svelte/elements' /** Selected range. `end` is undefined while the user has only picked the start. */ export interface DateRange { start: Date end: Date | undefined } /** * React DayPicker-shaped range (`{ from, to }`) for the `selected`/`onSelect` * aliases. Converts to/from the internal `{ start, end }` shape. Both use * native `Date`, so conversion is a key rename plus midnight normalization. */ export interface ReactDateRange { from: Date | undefined to?: Date | undefined } export interface CalendarDay { date: Date /** `y-m-d` key for focus management. */ key: string dayNumber: number outsideView: boolean today: boolean disabled: boolean unavailable: boolean selected: boolean selectionStart: boolean selectionEnd: boolean highlighted: boolean tabbable: boolean } export interface CalendarMonth { key: string label: string weeks: CalendarDay[][] } /** Context shared with the range-calendar parts. Getters stay reactive in children. */ export interface RangeCalendarContext { readonly months: CalendarMonth[] readonly weekDays: string[] readonly headingValue: string readonly disabled: boolean readonly readonly: boolean readonly canPrev: boolean readonly canNext: boolean select: (day: CalendarDay) => void hover: (day: CalendarDay | null) => void prev: () => void next: () => void focusDay: (fromKey: string, deltaDays: number) => void focusMonthStep: (fromKey: string, months: number) => void weekEdge: (fromKey: string, toStart: boolean) => void } // Omit `children`: the snippet carries months/weekDays, which narrows the base `Snippet` type. export interface RangeCalendarProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> { /** The controlled range. Bindable. When both `value` and `selected` are set, `value` wins. */ value?: DateRange | undefined /** Initial range for uncontrolled use. */ defaultValue?: DateRange | undefined /** Called with the new range after every commit. */ onValueChange?: (value: DateRange) => void /** * React DayPicker-parity alias for `value`. Accepts `{ from, to }` and * converts to internal `{ start, end }`. Bindable. Stays in sync with * `value`: user selection updates both, and external updates to either * sync the other. */ selected?: ReactDateRange | undefined /** React-parity alias for `onValueChange`. Fires with `{ from, to }` alongside `onValueChange`. */ onSelect?: (range: ReactDateRange | undefined) => void /** Earliest selectable date. */ minValue?: Date /** Latest selectable date. */ maxValue?: Date /** When `true`, prevents the user from interacting with the calendar. */ disabled?: boolean /** When `true`, dates cannot be selected but the calendar stays focusable. */ readonly?: boolean /** Always render 6 week rows so the height never shifts month-to-month. */ fixedWeeks?: boolean /** First day of the week: 0 = Sunday … 6 = Saturday. */ weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6 /** How many months to render side by side. */ numberOfMonths?: number /** BCP 47 locale for month and weekday names. Defaults to the runtime locale. */ locale?: string /** Initial visible month. Defaults to the start of `value`/`defaultValue`, else today. */ defaultPlaceholder?: Date /** Extra predicate to disable individual dates. */ isDateDisabled?: (date: Date) => boolean /** Extra predicate to mark individual dates unavailable. */ isDateUnavailable?: (date: Date) => boolean children?: Snippet<[{ months: CalendarMonth[]; weekDays: string[] }]> ref?: HTMLDivElement | null } </script> <script lang="ts"> import { setContext, tick } from 'svelte' import { cn } from '$lib/utils' import RangeCalendarCell from './RangeCalendarCell.svelte' import RangeCalendarCellTrigger from './RangeCalendarCellTrigger.svelte' import RangeCalendarGrid from './RangeCalendarGrid.svelte' import RangeCalendarGridBody from './RangeCalendarGridBody.svelte' import RangeCalendarGridHead from './RangeCalendarGridHead.svelte' import RangeCalendarGridRow from './RangeCalendarGridRow.svelte' import RangeCalendarHeadCell from './RangeCalendarHeadCell.svelte' import RangeCalendarHeader from './RangeCalendarHeader.svelte' import RangeCalendarHeading from './RangeCalendarHeading.svelte' import RangeCalendarNextButton from './RangeCalendarNextButton.svelte' import RangeCalendarPrevButton from './RangeCalendarPrevButton.svelte' let { class: className, value = $bindable(), defaultValue = undefined, onValueChange, selected = $bindable(), onSelect, minValue = undefined, maxValue = undefined, disabled = false, readonly = false, fixedWeeks = false, weekStartsOn = 0, numberOfMonths = 1, locale = undefined, defaultPlaceholder = undefined, isDateDisabled = undefined, isDateUnavailable = undefined, children, ref = $bindable(null), ...restProps }: RangeCalendarProps = $props() function atMidnight(d: Date): Date { return new Date(d.getFullYear(), d.getMonth(), d.getDate()) } function fromSelected(s: ReactDateRange | undefined): DateRange | undefined { if (!s?.from) return undefined return { start: atMidnight(s.from), end: s.to ? atMidnight(s.to) : undefined } } function toSelected(r: DateRange | undefined): ReactDateRange | undefined { if (!r) return undefined return { from: r.start, to: r.end } } function sameRange(a: DateRange | undefined, b: DateRange | undefined): boolean { if (a == null || b == null) return a == null && b == null return sameDay(a.start, b.start) && (a.end == null || b.end == null ? a.end == b.end : sameDay(a.end, b.end)) } function sameSelected(a: ReactDateRange | undefined, b: ReactDateRange | undefined): boolean { if (a == null || b == null) return a == null && b == null const fromSame = a.from == null || b.from == null ? a.from == b.from : sameDay(a.from, b.from) const toSame = a.to == null || b.to == null ? a.to == b.to : sameDay(a.to, b.to) return fromSame && toSame } function dayKey(d: Date): string { return `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}` } function sameDay(a: Date, b: Date): boolean { return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate() } function compareDay(a: Date, b: Date): number { const t = atMidnight(a).getTime() - atMidnight(b).getTime() return t === 0 ? 0 : t < 0 ? -1 : 1 } let root: HTMLDivElement | null = null // Uncontrolled seed: initial `value`/`selected` win over `defaultValue` so a // pre-selected controlled range shows its month on mount. // svelte-ignore state_referenced_locally let internal = $state<DateRange | undefined>(value ?? fromSelected(selected) ?? defaultValue) let hoverKey = $state<string | null>(null) // `value` wins when both aliases are set; `selected` converts `{ from, to }`. const current = $derived(value ?? fromSelected(selected) ?? internal) // Keep `value`/`selected` mirrors in sync on external updates (user selection // syncs via `commit` below). Last-write-wins, with `value` winning when both // change together. Guards prevent loops with `commit` (which updates all // three together, detected as already-in-sync with `internal`). let prevValue: DateRange | undefined = value let prevSelected: ReactDateRange | undefined = selected let didInitSync = false $effect(() => { const v = value const s = selected if (!didInitSync) { didInitSync = true prevValue = v prevSelected = s if (v !== undefined && s === undefined) { const toSel = toSelected(v) selected = toSel prevSelected = toSel } else if (s !== undefined && v === undefined) { const conv = fromSelected(s) value = conv prevValue = conv } else if (v !== undefined && s !== undefined) { if (!sameRange(v, fromSelected(s))) { const toSel = toSelected(v) selected = toSel prevSelected = toSel } } return } const vChanged = !sameRange(v, prevValue) const sChanged = !sameSelected(s, prevSelected) if (!vChanged && !sChanged) return if (vChanged && sChanged) { if (sameRange(v, internal) && sameSelected(s, toSelected(v))) { prevValue = v prevSelected = s return } if (v !== undefined) { internal = v const toSel = toSelected(v) if (!sameSelected(s, toSel)) selected = toSel prevValue = v prevSelected = toSel } else if (s !== undefined) { internal = fromSelected(s) prevValue = v prevSelected = s } else { internal = undefined prevValue = undefined prevSelected = undefined } } else if (vChanged) { internal = v const toSel = toSelected(v) if (!sameSelected(s, toSel)) selected = toSel prevValue = v prevSelected = toSel } else { const conv = fromSelected(s) internal = conv if (!sameRange(v, conv)) value = conv prevValue = conv prevSelected = s } }) // Visible month (first of the rendered run). Seeded from the placeholder, // else the range start, else today — so a pre-selected value shows its month. function initialVisible(): { y: number; m: number } { const seed = defaultPlaceholder ?? current?.start ?? new Date() return { y: seed.getFullYear(), m: seed.getMonth() } } // Uncontrolled seed: intentionally the initial placeholder/value only. // svelte-ignore state_referenced_locally let visible = $state(initialVisible()) $effect(() => { ref = root }) const today = $derived.by(() => { const now = new Date() return new Date(now.getFullYear(), now.getMonth(), now.getDate()) }) function isOutOfBounds(d: Date): boolean { if (minValue && compareDay(d, minValue) < 0) return true if (maxValue && compareDay(d, maxValue) > 0) return true return false } const months = $derived.by((): CalendarMonth[] => { const labelFmt = new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric' }) const range = current const start = range?.start ? atMidnight(range.start) : null const end = range?.end ? atMidnight(range.end) : null const hoverDate = hoverKey ? (() => { const [y, m, d] = hoverKey.split('-').map(Number) return new Date(y!, m!, d!) })() : null const inProgress = start && !end && hoverDate && !sameDay(start, hoverDate) const out: CalendarMonth[] = [] for (let i = 0; i < Math.max(1, numberOfMonths); i++) { const first = new Date(visible.y, visible.m + i, 1) const y = first.getFullYear() const m = first.getMonth() const dim = new Date(y, m + 1, 0).getDate() const leading = (first.getDay() - weekStartsOn + 7) % 7 let total = leading + dim const trailing = (7 - (total % 7)) % 7 total += trailing if (fixedWeeks) total = 42 const days: CalendarDay[] = [] for (let c = 0; c < total; c++) { const date = new Date(y, m, 1 - leading + c) const key = dayKey(date) const outsideView = date.getMonth() !== m const isToday = sameDay(date, today) const isDisabled = disabled || isOutOfBounds(date) || (isDateDisabled?.(date) ?? false) const isUnavailable = isDateUnavailable?.(date) ?? false const geStart = start ? compareDay(date, start) >= 0 : false const leEnd = end ? compareDay(date, end) <= 0 : false const isStart = start ? sameDay(date, start) : false const isEnd = end ? sameDay(date, end) : false const isSelected = end ? geStart && leEnd : isStart let highlighted = false if (inProgress && start && hoverDate) { const lo = compareDay(start, hoverDate) <= 0 ? start : hoverDate const hi = lo === start ? hoverDate : start highlighted = compareDay(date, lo) > 0 && compareDay(date, hi) < 0 } days.push({ date, key, dayNumber: date.getDate(), outsideView, today: isToday, disabled: isDisabled, unavailable: isUnavailable, selected: isSelected, selectionStart: isStart, selectionEnd: isEnd, highlighted, tabbable: false, }) } const weeks: CalendarDay[][] = [] for (let w = 0; w < days.length; w += 7) weeks.push(days.slice(w, w + 7)) out.push({ key: `${y}-${m}`, label: labelFmt.format(first), weeks }) } // One tabbable day across the whole run: range start, else today, else first enabled day. const flat = out.flatMap((mo) => mo.weeks.flat()) const pick = (start && flat.find((d) => sameDay(d.date, start) && !d.disabled)) || flat.find((d) => d.today && !d.disabled) || flat.find((d) => !d.disabled && !d.outsideView) || flat.find((d) => !d.disabled) if (pick) pick.tabbable = true return out }) const weekDays = $derived.by(() => { const fmt = new Intl.DateTimeFormat(locale, { weekday: 'narrow' }) // 2024-01-07 was a Sunday; walk a full week from there. const names = Array.from({ length: 7 }, (_, i) => fmt.format(new Date(2024, 0, 7 + i))) return [...names.slice(weekStartsOn), ...names.slice(0, weekStartsOn)] }) const headingValue = $derived(months.map((mo) => mo.label).join(' – ')) const canPrev = $derived.by(() => { if (!minValue) return true const first = new Date(visible.y, visible.m, 1) const minMonth = new Date(minValue.getFullYear(), minValue.getMonth(), 1) return first > minMonth }) const canNext = $derived.by(() => { if (!maxValue) return true const count = Math.max(1, numberOfMonths) const last = new Date(visible.y, visible.m + count - 1, 1) const maxMonth = new Date(maxValue.getFullYear(), maxValue.getMonth(), 1) return last < maxMonth }) function commit(next: DateRange) { internal = next value = next const sel = toSelected(next) selected = sel onValueChange?.(next) onSelect?.(sel) } function select(day: CalendarDay) { if (disabled || readonly || day.disabled || day.unavailable) return const d = atMidnight(day.date) const range = current if (!range?.start || range.end) { commit({ start: d, end: undefined }) } else if (compareDay(d, range.start) < 0) { commit({ start: d, end: atMidnight(range.start) }) } else { commit({ start: atMidnight(range.start), end: d }) } } function hover(day: CalendarDay | null) { hoverKey = day ? day.key : null } function prev() { if (!canPrev) return const d = new Date(visible.y, visible.m - 1, 1) visible = { y: d.getFullYear(), m: d.getMonth() } } function next() { if (!canNext) return const d = new Date(visible.y, visible.m + 1, 1) visible = { y: d.getFullYear(), m: d.getMonth() } } function focusKey(key: string) { const target = root?.querySelector<HTMLButtonElement>(`[data-day="${key}"]`) target?.focus() } function parseKey(key: string): Date { const [y, m, d] = key.split('-').map(Number) return new Date(y!, m!, d!) } /** Move focus by whole days, shifting the visible window when leaving it. */ function focusDay(fromKey: string, deltaDays: number) { const target = parseKey(fromKey) target.setDate(target.getDate() + deltaDays) const count = Math.max(1, numberOfMonths) const firstVisible = new Date(visible.y, visible.m, 1) const lastVisible = new Date(visible.y, visible.m + count, 0) if (compareDay(target, firstVisible) < 0) { visible = { y: target.getFullYear(), m: target.getMonth() } tick().then(() => focusKey(dayKey(target))) } else if (compareDay(target, lastVisible) > 0) { visible = { y: target.getFullYear(), m: target.getMonth() - count + 1 } tick().then(() => focusKey(dayKey(target))) } else { focusKey(dayKey(target)) } } /** Move focus by whole months (PageUp/PageDown), clamping the day number. */ function focusMonthStep(fromKey: string, step: number) { const from = parseKey(fromKey) const target = new Date(from.getFullYear(), from.getMonth() + step, 1) const dim = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate() target.setDate(Math.min(from.getDate(), dim)) visible = { y: target.getFullYear(), m: target.getMonth() } tick().then(() => focusKey(dayKey(target))) } /** Move focus to the start/end of the focused week row (Home/End). */ function weekEdge(fromKey: string, toStart: boolean) { const from = parseKey(fromKey) const offset = (from.getDay() - weekStartsOn + 7) % 7 focusDay(fromKey, toStart ? -offset : 6 - offset) } setContext<RangeCalendarContext>('rangeCalendar', { get months() { return months }, get weekDays() { return weekDays }, get headingValue() { return headingValue }, get disabled() { return disabled }, get readonly() { return readonly }, get canPrev() { return canPrev }, get canNext() { return canNext }, select, hover, prev, next, focusDay, focusMonthStep, weekEdge, }) </script> <div bind:this={root} data-uipkge="" data-slot="range-calendar" data-disabled={disabled || undefined} class={cn('p-3', className)} {...restProps} > <RangeCalendarHeader> <RangeCalendarHeading /> <div class="flex items-center gap-1"> <RangeCalendarPrevButton /> <RangeCalendarNextButton /> </div> </RangeCalendarHeader> <div class="mt-4 flex flex-col gap-y-4 sm:flex-row sm:gap-x-4 sm:gap-y-0"> {#each months as month (month.key)} <RangeCalendarGrid label={month.label}> <RangeCalendarGridHead> <RangeCalendarGridRow> {#each weekDays as day, di (di)} <RangeCalendarHeadCell> {day} </RangeCalendarHeadCell> {/each} </RangeCalendarGridRow> </RangeCalendarGridHead> <RangeCalendarGridBody> {#each month.weeks as week, index (index)} <RangeCalendarGridRow class="mt-2 w-full"> {#each week as weekDate (weekDate.key)} <RangeCalendarCell> <RangeCalendarCellTrigger day={weekDate} /> </RangeCalendarCell> {/each} </RangeCalendarGridRow> {/each} </RangeCalendarGridBody> </RangeCalendarGrid> {/each} </div> {@render children?.({ months, weekDays })} </div>src/lib/components/ui/range-calendar/RangeCalendarCell.svelte1 kB
<script lang="ts" module> import type { Snippet } from 'svelte' import type { HTMLAttributes } from 'svelte/elements' export interface RangeCalendarCellProps extends HTMLAttributes<HTMLTableCellElement> { children?: Snippet ref?: HTMLTableCellElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' let { class: className, children, ref = $bindable(null), ...restProps }: RangeCalendarCellProps = $props() </script> <td bind:this={ref} data-uipkge="" data-slot="range-calendar-cell" class={cn( '[&:has([data-selected])]:bg-accent [&:has([data-highlighted])]:bg-accent relative p-0 text-center text-sm focus-within:relative focus-within:z-20 first:[&:has([data-selected])]:rounded-l-md last:[&:has([data-selected])]:rounded-r-md [&:has([data-selected][data-selection-end])]:rounded-r-md [&:has([data-selected][data-selection-start])]:rounded-l-md', className, )} {...restProps} > {@render children?.()} </td>src/lib/components/ui/range-calendar/RangeCalendarCellTrigger.svelte4.8 kB
<script lang="ts" module> import type { Snippet } from 'svelte' import type { HTMLButtonAttributes } from 'svelte/elements' import type { CalendarDay, RangeCalendarContext } from './RangeCalendar.svelte' // Omit `children`: the snippet carries the day, which narrows the base `Snippet` type. export interface RangeCalendarCellTriggerProps extends Omit<HTMLButtonAttributes, 'children'> { day: CalendarDay children?: Snippet<[{ day: CalendarDay }]> ref?: HTMLButtonElement | null } </script> <script lang="ts"> import { getContext } from 'svelte' import { cn } from '$lib/utils' import { buttonVariants } from '$lib/components/ui/button/button.variants' let { class: className, day, children, disabled = undefined, onclick: onclickProp, onkeydown: onkeydownProp, ref = $bindable(null), ...restProps }: RangeCalendarCellTriggerProps = $props() const calendar = getContext<RangeCalendarContext | undefined>('rangeCalendar') const effectiveDisabled = $derived(disabled ?? day.disabled ?? false) function handleKeydown(event: KeyboardEvent) { if (!calendar) return if (event.key === 'ArrowRight') { event.preventDefault() calendar.focusDay(day.key, 1) } else if (event.key === 'ArrowLeft') { event.preventDefault() calendar.focusDay(day.key, -1) } else if (event.key === 'ArrowDown') { event.preventDefault() calendar.focusDay(day.key, 7) } else if (event.key === 'ArrowUp') { event.preventDefault() calendar.focusDay(day.key, -7) } else if (event.key === 'Home') { event.preventDefault() calendar.weekEdge(day.key, true) } else if (event.key === 'End') { event.preventDefault() calendar.weekEdge(day.key, false) } else if (event.key === 'PageUp') { event.preventDefault() calendar.focusMonthStep(day.key, event.shiftKey ? -12 : -1) } else if (event.key === 'PageDown') { event.preventDefault() calendar.focusMonthStep(day.key, event.shiftKey ? 12 : 1) } } </script> <button bind:this={ref} type="button" data-uipkge="" data-slot="range-calendar-trigger" data-day={day.key} data-selected={day.selected || undefined} data-selection-start={day.selectionStart || undefined} data-selection-end={day.selectionEnd || undefined} data-highlighted={day.highlighted || undefined} data-today={day.today || undefined} data-outside-view={day.outsideView || undefined} data-unavailable={day.unavailable || undefined} disabled={effectiveDisabled} tabindex={day.tabbable ? 0 : -1} aria-label={day.date.toLocaleDateString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })} aria-pressed={day.selected || undefined} class={cn( buttonVariants({ variant: 'ghost' }), 'size-9 cursor-pointer p-0 font-normal transition-[color,background-color,transform,box-shadow] duration-150 data-[selected]:opacity-100', '[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground', // Selection Start / End — subtle pop 'data-[selection-start]:bg-primary data-[selection-start]:text-primary-foreground data-[selection-start]:hover:bg-primary data-[selection-start]:hover:text-primary-foreground data-[selection-start]:focus:bg-primary data-[selection-start]:focus:text-primary-foreground motion-safe:data-[selection-start]:animate-[calendar-day-pop_220ms_cubic-bezier(0.22,1.4,0.36,1)_both]', 'data-[selection-end]:bg-primary data-[selection-end]:text-primary-foreground data-[selection-end]:hover:bg-primary data-[selection-end]:hover:text-primary-foreground data-[selection-end]:focus:bg-primary data-[selection-end]:focus:text-primary-foreground motion-safe:data-[selection-end]:animate-[calendar-day-pop_220ms_cubic-bezier(0.22,1.4,0.36,1)_both]', // Outside months 'data-[outside-view]:text-muted-foreground', // Disabled 'data-[disabled]:text-muted-foreground data-[disabled]:opacity-50', // Unavailable 'data-[unavailable]:text-destructive-foreground data-[unavailable]:line-through', className, )} {...restProps} onclick={(e) => { calendar?.select(day) onclickProp?.(e) }} onkeydown={(e) => { handleKeydown(e) onkeydownProp?.(e) }} onmouseenter={() => calendar?.hover(day)} onmouseleave={() => calendar?.hover(null)} onfocus={() => calendar?.hover(null)} > {#if children} {@render children({ day })} {:else} {day.dayNumber} {/if} </button> <style> @keyframes calendar-day-pop { 0% { transform: scale(0.86); } 70% { transform: scale(1.06); } 100% { transform: scale(1); } } @media (prefers-reduced-motion: reduce) { [data-slot='range-calendar-trigger'] { animation: none !important; transition: none !important; } } </style>src/lib/components/ui/range-calendar/RangeCalendarGrid.svelte0.8 kB
<script lang="ts" module> import type { Snippet } from 'svelte' import type { HTMLAttributes } from 'svelte/elements' export interface RangeCalendarGridProps extends HTMLAttributes<HTMLTableElement> { /** Accessible month label, used as the table caption for screen readers. */ label?: string children?: Snippet ref?: HTMLTableElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' let { class: className, label = undefined, children, ref = $bindable(null), ...restProps }: RangeCalendarGridProps = $props() </script> <table bind:this={ref} data-uipkge="" data-slot="range-calendar-grid" role="grid" aria-label={label} class={cn('w-full border-collapse', className)} {...restProps} > {@render children?.()} </table>src/lib/components/ui/range-calendar/RangeCalendarGridBody.svelte0.5 kB
<script lang="ts" module> import type { Snippet } from 'svelte' import type { HTMLAttributes } from 'svelte/elements' export interface RangeCalendarGridBodyProps extends HTMLAttributes<HTMLTableSectionElement> { children?: Snippet ref?: HTMLTableSectionElement | null } </script> <script lang="ts"> let { children, ref = $bindable(null), ...restProps }: RangeCalendarGridBodyProps = $props() </script> <tbody bind:this={ref} data-uipkge="" data-slot="range-calendar-grid-body" {...restProps}> {@render children?.()} </tbody>src/lib/components/ui/range-calendar/RangeCalendarGridHead.svelte0.5 kB
<script lang="ts" module> import type { Snippet } from 'svelte' import type { HTMLAttributes } from 'svelte/elements' export interface RangeCalendarGridHeadProps extends HTMLAttributes<HTMLTableSectionElement> { children?: Snippet ref?: HTMLTableSectionElement | null } </script> <script lang="ts"> let { children, ref = $bindable(null), ...restProps }: RangeCalendarGridHeadProps = $props() </script> <thead bind:this={ref} data-uipkge="" data-slot="range-calendar-grid-head" {...restProps}> {@render children?.()} </thead>src/lib/components/ui/range-calendar/RangeCalendarGridRow.svelte0.6 kB
<script lang="ts" module> import type { Snippet } from 'svelte' import type { HTMLAttributes } from 'svelte/elements' export interface RangeCalendarGridRowProps extends HTMLAttributes<HTMLTableRowElement> { children?: Snippet ref?: HTMLTableRowElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' let { class: className, children, ref = $bindable(null), ...restProps }: RangeCalendarGridRowProps = $props() </script> <tr bind:this={ref} data-uipkge="" data-slot="range-calendar-grid-row" class={cn('flex', className)} {...restProps}> {@render children?.()} </tr>src/lib/components/ui/range-calendar/RangeCalendarHeadCell.svelte0.7 kB
<script lang="ts" module> import type { Snippet } from 'svelte' import type { HTMLAttributes } from 'svelte/elements' export interface RangeCalendarHeadCellProps extends HTMLAttributes<HTMLTableCellElement> { children?: Snippet ref?: HTMLTableCellElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' let { class: className, children, ref = $bindable(null), ...restProps }: RangeCalendarHeadCellProps = $props() </script> <th bind:this={ref} data-uipkge="" data-slot="range-calendar-head-cell" scope="col" class={cn('text-muted-foreground w-8 rounded-md text-[0.8rem] font-normal', className)} {...restProps} > {@render children?.()} </th>src/lib/components/ui/range-calendar/RangeCalendarHeader.svelte0.6 kB
<script lang="ts" module> import type { Snippet } from 'svelte' import type { HTMLAttributes } from 'svelte/elements' export interface RangeCalendarHeaderProps extends HTMLAttributes<HTMLDivElement> { children?: Snippet ref?: HTMLDivElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' let { class: className, children, ref = $bindable(null), ...restProps }: RangeCalendarHeaderProps = $props() </script> <div bind:this={ref} data-uipkge="" data-slot="range-calendar-header" class={cn('relative flex w-full items-center justify-center pt-1', className)} {...restProps} > {@render children?.()} </div>src/lib/components/ui/range-calendar/RangeCalendarHeading.svelte1.1 kB
<script lang="ts" module> import type { Snippet } from 'svelte' import type { HTMLAttributes } from 'svelte/elements' import type { RangeCalendarContext } from './RangeCalendar.svelte' // Omit `children`: the snippet carries headingValue, which narrows the base `Snippet` type. export interface RangeCalendarHeadingProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> { children?: Snippet<[{ headingValue: string }]> ref?: HTMLDivElement | null } </script> <script lang="ts"> import { getContext } from 'svelte' import { cn } from '$lib/utils' let { class: className, children, ref = $bindable(null), ...restProps }: RangeCalendarHeadingProps = $props() const calendar = getContext<RangeCalendarContext | undefined>('rangeCalendar') const headingValue = $derived(calendar?.headingValue ?? '') </script> <div bind:this={ref} data-uipkge="" data-slot="range-calendar-heading" aria-live="polite" class={cn('text-sm font-medium', className)} {...restProps} > {#if children} {@render children({ headingValue })} {:else} {headingValue} {/if} </div>src/lib/components/ui/range-calendar/RangeCalendarNextButton.svelte1.5 kB
<script lang="ts" module> import type { Snippet } from 'svelte' import type { HTMLButtonAttributes } from 'svelte/elements' import type { RangeCalendarContext } from './RangeCalendar.svelte' export interface RangeCalendarNextButtonProps extends HTMLButtonAttributes { children?: Snippet ref?: HTMLButtonElement | null } </script> <script lang="ts"> import { getContext } from 'svelte' import { ChevronRight } from '@lucide/svelte' import { cn } from '$lib/utils' import { buttonVariants } from '$lib/components/ui/button/button.variants' let { class: className, children, disabled = undefined, onclick: onclickProp, ref = $bindable(null), ...restProps }: RangeCalendarNextButtonProps = $props() const calendar = getContext<RangeCalendarContext | undefined>('rangeCalendar') const effectiveDisabled = $derived(disabled ?? (calendar ? calendar.disabled || !calendar.canNext : true)) </script> <button bind:this={ref} type="button" data-uipkge="" data-slot="range-calendar-next-button" aria-label="Next month" disabled={effectiveDisabled} class={cn( buttonVariants({ variant: 'outline' }), 'absolute right-1', 'size-7 bg-transparent p-0 opacity-50 hover:opacity-100', className, )} {...restProps} onclick={(e) => { calendar?.next() onclickProp?.(e) }} > {#if children} {@render children()} {:else} <ChevronRight class="size-4" aria-hidden="true" /> {/if} </button>src/lib/components/ui/range-calendar/RangeCalendarPrevButton.svelte1.5 kB
<script lang="ts" module> import type { Snippet } from 'svelte' import type { HTMLButtonAttributes } from 'svelte/elements' import type { RangeCalendarContext } from './RangeCalendar.svelte' export interface RangeCalendarPrevButtonProps extends HTMLButtonAttributes { children?: Snippet ref?: HTMLButtonElement | null } </script> <script lang="ts"> import { getContext } from 'svelte' import { ChevronLeft } from '@lucide/svelte' import { cn } from '$lib/utils' import { buttonVariants } from '$lib/components/ui/button/button.variants' let { class: className, children, disabled = undefined, onclick: onclickProp, ref = $bindable(null), ...restProps }: RangeCalendarPrevButtonProps = $props() const calendar = getContext<RangeCalendarContext | undefined>('rangeCalendar') const effectiveDisabled = $derived(disabled ?? (calendar ? calendar.disabled || !calendar.canPrev : true)) </script> <button bind:this={ref} type="button" data-uipkge="" data-slot="range-calendar-prev-button" aria-label="Previous month" disabled={effectiveDisabled} class={cn( buttonVariants({ variant: 'outline' }), 'absolute left-1', 'size-7 bg-transparent p-0 opacity-50 hover:opacity-100', className, )} {...restProps} onclick={(e) => { calendar?.prev() onclickProp?.(e) }} > {#if children} {@render children()} {:else} <ChevronLeft class="size-4" aria-hidden="true" /> {/if} </button>src/lib/components/ui/range-calendar/index.ts1.4 kB
export { default as RangeCalendar, type CalendarDay, type CalendarMonth, type DateRange, type ReactDateRange, type RangeCalendarContext, type RangeCalendarProps, } from './RangeCalendar.svelte' export { default as RangeCalendarCell, type RangeCalendarCellProps } from './RangeCalendarCell.svelte' export { default as RangeCalendarCellTrigger, type RangeCalendarCellTriggerProps, } from './RangeCalendarCellTrigger.svelte' export { default as RangeCalendarGrid, type RangeCalendarGridProps } from './RangeCalendarGrid.svelte' export { default as RangeCalendarGridBody, type RangeCalendarGridBodyProps } from './RangeCalendarGridBody.svelte' export { default as RangeCalendarGridHead, type RangeCalendarGridHeadProps } from './RangeCalendarGridHead.svelte' export { default as RangeCalendarGridRow, type RangeCalendarGridRowProps } from './RangeCalendarGridRow.svelte' export { default as RangeCalendarHeadCell, type RangeCalendarHeadCellProps } from './RangeCalendarHeadCell.svelte' export { default as RangeCalendarHeader, type RangeCalendarHeaderProps } from './RangeCalendarHeader.svelte' export { default as RangeCalendarHeading, type RangeCalendarHeadingProps } from './RangeCalendarHeading.svelte' export { default as RangeCalendarNextButton, type RangeCalendarNextButtonProps, } from './RangeCalendarNextButton.svelte' export { default as RangeCalendarPrevButton, type RangeCalendarPrevButtonProps, } from './RangeCalendarPrevButton.svelte'
Raw manifest:https://uipkge.dev/r/svelte/range-calendar.json