Date Range Field
Allows users to input a range of dates within a designated field.
<script lang="ts">
import { DateRangeField } from "bits-ui";
</script>
<DateRangeField.Root class="group flex w-full max-w-[320px] flex-col gap-1.5">
<DateRangeField.Label class="block select-none text-sm font-medium">
Hotel dates
</DateRangeField.Label>
<div
class="h-input rounded-input border-border-input bg-background text-foreground focus-within:border-border-input-hover focus-within:shadow-date-field-focus hover:border-border-input-hover group-data-invalid:border-destructive flex w-full select-none items-center border px-2 py-3 text-sm tracking-[0.01em]"
>
{#each ["start", "end"] as const as type}
<DateRangeField.Input {type}>
{#snippet children({ segments })}
{#each segments as { part, value }}
<div class="inline-block select-none">
{#if part === "literal"}
<DateRangeField.Segment
{part}
class="text-muted-foreground p-1"
>
{value}
</DateRangeField.Segment>
{:else}
<DateRangeField.Segment
{part}
class="rounded-5px hover:bg-muted focus:bg-muted focus:text-foreground aria-[valuetext=Empty]:text-muted-foreground focus-visible:ring-0! focus-visible:ring-offset-0! px-1 py-1"
>
{value}
</DateRangeField.Segment>
{/if}
</div>
{/each}
{/snippet}
</DateRangeField.Input>
{#if type === "start"}
<div aria-hidden="true" class="text-muted-foreground px-1">–</div>
{/if}
{/each}
</div>
</DateRangeField.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!
Overview
The DateRangeField
component combines two Date Field components to create a date range field. Check out the Date Field component documentation for information on how to customize this component.
Structure
<script lang="ts">
import { DateRangeField } from "$lib";
</script>
<DateRangeField.Root>
<DateRangeField.Label>Check-in date</DateRangeField.Label>
{#each ["start", "end"] as const as type}
<DateRangeField.Input {type}>
{#snippet children({ segments })}
{#each segments as { part, value }}
<DateRangeField.Segment {part}>
{value}
</DateRangeField.Segment>
{/each}
{/snippet}
</DateRangeField.Input>
{/each}
</DateRangeField.Root>
Managing Placeholder State
This section covers how to manage the placeholder
state of the component.
Two-Way Binding
Use bind:placeholder
for simple, automatic state synchronization:
<script lang="ts">
import { DateRangeField } from "bits-ui";
import { CalendarDateTime } from "@internationalized/date";
let myPlaceholder = $state(new CalendarDateTime(2024, 8, 3, 12, 30));
</script>
<DateRangeField.Root bind:placeholder={myPlaceholder}>
<!-- ... -->
</DateRangeField.Root>
Fully Controlled
Use a Function Binding for complete control over the state's reads and writes.
<script lang="ts">
import { DateRangeField } from "bits-ui";
import { CalendarDateTime } from "@internationalized/date";
let myPlaceholder = $state(new CalendarDateTime(2024, 8, 3, 12, 30));
function getPlaceholder() {
return myPlaceholder;
}
function setPlaceholder(newPlaceholder: CalendarDateTime) {
myPlaceholder = newPlaceholder;
}
</script>
<DateRangeField.Root bind:placeholder={getPlaceholder, setPlaceholder}>
<!-- ... -->
</DateRangeField.Root>
Managing Value State
This section covers how to manage the value
state of the component.
Two-Way Binding
Use bind:value
for simple, automatic state synchronization:
<script lang="ts">
import { DateRangeField, type DateRange } from "bits-ui";
import { CalendarDateTime } from "@internationalized/date";
let myValue = $state<DateRange>({
start: new CalendarDateTime(2024, 8, 3, 12, 30),
end: new CalendarDateTime(2024, 8, 4, 12, 30),
});
</script>
<button
onclick={() => {
value = {
start: value.start.add({ days: 1 }),
end: value.end.add({ days: 1 }),
};
}}
>
Add 1 day
</button>
<DateRangeField.Root bind:value={myValue}>
<!-- ... -->
</DateRangeField.Root>
Fully Controlled
Use a Function Binding for complete control over the state's reads and writes.
<script lang="ts">
import { DateRangeField } from "bits-ui";
let myValue = $state<DateRange>({
start: undefined,
end: undefined,
});
function getValue() {
return myValue;
}
function setValue(newValue: DateRange) {
myValue = newValue;
}
</script>
<DateRangeField.Root bind:value={getValue, setValue}>
<!-- ... -->
</DateRangeField.Root>
API Reference
The root date field component.
Property | Type | Description |
---|---|---|
value $bindable | DateRange | The selected date range. Default: undefined |
onValueChange | function | A function that is called when the selected date changes. Default: undefined |
placeholder $bindable | DateValue | The placeholder date, which is used to determine what date to start the segments from when no value exists. Default: undefined |
onPlaceholderChange | function | A function that is called when the placeholder date changes. Default: undefined |
errorMessageId | string | The Default: undefined |
validate | function | A function that returns whether or not a date is unavailable. Default: undefined |
onInvalid | function | A callback fired when the date field's value is invalid. Default: undefined |
minValue | DateValue | The minimum valid date that can be entered. Default: undefined |
maxValue | DateValue | The maximum valid date that can be entered. Default: undefined |
granularity | enum | The granularity to use for formatting the field. Defaults to Default: undefined |
hideTimeZone | boolean | Whether or not to hide the time zone segment of the field. Default: false |
hourCycle | enum | The hour cycle to use for formatting times. Defaults to the locale preference Default: undefined |
locale | string | The locale to use for formatting dates. Default: 'en-US' |
disabled | boolean | Whether or not the accordion is disabled. Default: false |
readonly | boolean | Whether or not the field is readonly. Default: false |
readonlySegments | EditableSegmentPart[] | An array of segments that should be readonly, which prevent user input on them. Default: undefined |
required | boolean | Whether or not the date field is required. 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-date-range-field-root | '' | Present on the root element. |
The container for the segments of the date field.
Property | Type | Description |
---|---|---|
type required | enum | The type of field to render (start or end). Default: undefined |
name | string | The name of the date field used for form submission. If provided, a hidden input element will be rendered alongside the date field. 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 element when the field is invalid. |
data-disabled | '' | Present on the element when the field is disabled. |
data-date-field-input | '' | Present on the element. |
A segment of the date field.
Property | Type | Description |
---|---|---|
part required | SegmentPart | The part of the date to render. Default: undefined |
ref $bindable | HTMLSpanElement | 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 element when the field is invalid |
data-disabled | '' | Present on the element when the field is disabled |
data-segment | enum | The type of segment the element represents. |
data-date-field-segment | '' | Present on the element. |
The label for the date field.
Property | Type | Description |
---|---|---|
ref $bindable | HTMLSpanElement | 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 element when the field is invalid |
data-date-field-label | '' | Present on the element. |