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="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="flex h-input w-full select-none items-center rounded-input border border-border-input bg-background px-2 py-3 text-sm tracking-[0.01em] text-foreground focus-within:border-border-input-hover focus-within:shadow-date-field-focus hover:border-border-input-hover"
>
{#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="p-1 text-muted-foreground"
>
{value}
</DateRangeField.Segment>
{:else}
<DateRangeField.Segment
{part}
class="rounded-5px px-1 py-1 hover:bg-muted focus:bg-muted focus:text-foreground focus-visible:!ring-0 focus-visible:!ring-offset-0 aria-[valuetext=Empty]:text-muted-foreground"
>
{value}
</DateRangeField.Segment>
{/if}
</div>
{/each}
{/snippet}
</DateRangeField.Input>
{#if type === "start"}
<div aria-hidden="true" class="px-1 text-muted-foreground">–</div>
{/if}
{/each}
</div>
</DateRangeField.Root>
import typography from "@tailwindcss/typography";
import animate from "tailwindcss-animate";
import { fontFamily } from "tailwindcss/defaultTheme";
/** @type {import('tailwindcss').Config} */
export default {
darkMode: "class",
content: ["./src/**/*.{html,js,svelte,ts}"],
theme: {
container: {
center: true,
screens: {
"2xl": "1440px",
},
},
extend: {
colors: {
border: {
DEFAULT: "hsl(var(--border-card))",
input: "hsl(var(--border-input))",
"input-hover": "hsl(var(--border-input-hover))",
},
background: {
DEFAULT: "hsl(var(--background) / <alpha-value>)",
alt: "hsl(var(--background-alt) / <alpha-value>)",
},
foreground: {
DEFAULT: "hsl(var(--foreground) / <alpha-value>)",
alt: "hsl(var(--foreground-alt) / <alpha-value>)",
},
muted: {
DEFAULT: "hsl(var(--muted) / <alpha-value>)",
foreground: "hsl(var(--muted-foreground))",
},
dark: {
DEFAULT: "hsl(var(--dark) / <alpha-value>)",
4: "hsl(var(--dark-04))",
10: "hsl(var(--dark-10))",
40: "hsl(var(--dark-40))",
},
accent: {
DEFAULT: "hsl(var(--accent) / <alpha-value>)",
foreground: "hsl(var(--accent-foreground) / <alpha-value>)",
},
destructive: {
DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
},
contrast: {
DEFAULT: "hsl(var(--contrast) / <alpha-value>)",
},
},
fontFamily: {
sans: ["Inter", ...fontFamily.sans],
mono: ["Source Code Pro", ...fontFamily.mono],
alt: ["Courier", ...fontFamily.sans],
},
fontSize: {
xxs: "10px",
},
borderWidth: {
6: "6px",
},
borderRadius: {
card: "16px",
"card-lg": "20px",
"card-sm": "10px",
input: "9px",
button: "5px",
"5px": "5px",
"9px": "9px",
"10px": "10px",
"15px": "15px",
},
height: {
input: "3rem",
"input-sm": "2.5rem",
},
boxShadow: {
mini: "var(--shadow-mini)",
"mini-inset": "var(--shadow-mini-inset)",
popover: "var(--shadow-popover)",
kbd: "var(--shadow-kbd)",
btn: "var(--shadow-btn)",
card: "var(--shadow-card)",
"date-field-focus": "var(--shadow-date-field-focus)",
},
opacity: {
8: "0.08",
},
scale: {
80: ".80",
98: ".98",
99: ".99",
},
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--bits-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--bits-accordion-content-height)" },
to: { height: "0" },
},
"caret-blink": {
"0%,70%,100%": { opacity: "1" },
"20%,50%": { opacity: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
"caret-blink": "caret-blink 1.25s ease-out infinite",
},
},
plugins: [typography, animate],
};
@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");
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
/* Colors */
--background: 0 0% 100%;
--background-alt: 0 0% 100%;
--foreground: 0 0% 9%;
--foreground-alt: 0 0% 32%;
--muted: 240 5% 96%;
--muted-foreground: 0 0% 9% / 0.4;
--border: 240 6% 10%;
--border-input: 240 6% 10% / 0.17;
--border-input-hover: 240 6% 10% / 0.4;
--border-card: 240 6% 10% / 0.1;
--dark: 240 6% 10%;
--dark-10: 240 6% 10% / 0.1;
--dark-40: 240 6% 10% / 0.4;
--dark-04: 240 6% 10% / 0.04;
--accent: 204 94% 94%;
--accent-foreground: 204 80% 16%;
--destructive: 347 77% 50%;
/* black */
--constrast: 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: 0 0% 5%;
--background-alt: 0 0% 8%;
--foreground: 0 0% 95%;
--foreground-alt: 0 0% 70%;
--muted: 240 4% 16%;
--muted-foreground: 0 0% 100% / 0.4;
--border: 0 0% 96%;
--border-input: 0 0% 96% / 0.17;
--border-input-hover: 0 0% 96% / 0.4;
--border-card: 0 0% 96% / 0.1;
--dark: 0 0% 96%;
--dark-40: 0 0% 96% / 0.4;
--dark-10: 0 0% 96% / 0.1;
--dark-04: 0 0% 96% / 0.04;
--accent: 204 90 90%;
--accent-foreground: 204 94% 94%;
--destructive: 350 89% 60%;
/* white */
--constrast: 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);
}
}
@layer base {
* {
@apply border-border;
}
html {
-webkit-text-size-adjust: 100%;
font-variation-settings: normal;
}
body {
@apply bg-background text-foreground;
font-feature-settings:
"rlig" 1,
"calt" 1;
}
/* Mobile tap highlight */
/* https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-tap-highlight-color */
html {
-webkit-tap-highlight-color: rgba(128, 128, 128, 0.5);
}
::selection {
background: #fdffa4;
color: black;
}
/* === Scrollbars === */
::-webkit-scrollbar {
@apply w-2;
@apply h-2;
}
::-webkit-scrollbar-track {
@apply !bg-transparent;
}
::-webkit-scrollbar-thumb {
@apply rounded-card-lg !bg-dark-10;
}
::-webkit-scrollbar-corner {
background: rgba(0, 0, 0, 0);
}
/* Firefox */
/* https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color#browser_compatibility */
html {
scrollbar-color: var(--bg-muted);
}
.antialised {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
@layer utilities {
.step {
counter-increment: step;
}
.step:before {
@apply absolute inline-flex h-9 w-9 items-center justify-center rounded-full border-4 border-background bg-muted text-center -indent-px font-mono text-base font-medium;
@apply ml-[-50px] mt-[-4px];
content: counter(step);
}
}
@layer components {
*:not(body):not(.focus-override) {
outline: none !important;
&:focus-visible {
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background;
}
}
.link {
@apply inline-flex items-center gap-1 rounded-sm font-medium underline underline-offset-4 hover:text-foreground/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type="number"] {
-moz-appearance: textfield;
}
}
Structure
<script lang="ts">
import { DateField } 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>
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!
Placeholder State
Bits UI provides flexible options for controlling and synchronizing the DateRangeField
component's placeholder
state.
Two-Way Binding
Use the bind:placeholder
directive for effortless two-way synchronization between your local state and the DateRangeField
component's placeholder.
<script lang="ts">
import { DateRangeField } from "bits-ui";
let placeholder = $state(new CalendarDateTime(2024, 8, 3, 12, 30));
</script>
<DateRangeField.Root bind:placeholder>
<!-- ... -->
</DateField.Root>
This setup enables toggling the DateRangeField
component's placeholder via the custom button and ensures the local placeholder
state is synchronized with the DateRangeField
component's placeholder should it change from within the component.
Change Handler
You can also use the onPlaceholderChange
prop to update local state when the component's placeholder
changes. This is useful when you don't want two-way binding for one reason or another, or you want to perform additional logic when the DateRangeField
component's placeholder changes.
<script lang="ts">
import { DateRangeField } from "bits-ui";
let placeholder = $state(new CalendarDateTime(2024, 8, 3, 12, 30));
</script>
<DateRangeField.Root
bind:placeholder
onPlaceholderChange={(p) => {
placeholder = placeholder.set({ year: 2025 });
}}
>
<!-- ... -->
</DateRangeField.Root>
Controlled
Sometimes, you may want complete control over the placeholder
state, meaning you will be "kept in the loop" and be required to apply the state change yourself. While you will rarely need this, it's possible to do so by setting the controlledPlaceholder
prop to true
.
You will then be responsible for updating a local placeholder state variable that is passed as the placeholder
prop to the DateRangeField.Root
component.
<script lang="ts">
import { DateRangeField } from "bits-ui";
let myPlaceholder = $state();
</script>
<DateRangeField.Root
controlledPlaceholder
placeholder={myPlaceholder}
onPlaceholderChange={(p) => (myPlaceholder = p)}
>
<!-- ... -->
</DateRangeField.Root>
See the Controlled State documentation for more information about controlled states.
Value State
The value
represents the currently selected date within the DateRangeField
component.
Bits UI provides flexible options for controlling and synchronizing the DateRangeField
component's value state.
Two-Way Binding
Use the bind:value
directive for effortless two-way synchronization between your local state and the DateRangeField
component's value.
<script lang="ts">
import { DateRangeField } from "bits-ui";
let value = $state({
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>
<!-- ... -->
</DateRangeField.Root>
This setup enables toggling the component's value via the custom button and ensures the local value
state is synchronized with the component's value, should it change from within the component.
Change Handler
You can also use the onValueChange
prop to update local state when the component's value changes. This is useful when you don't want two-way binding for one reason or another, or you want to perform additional logic when the component's value changes.
<script lang="ts">
import { DateRangeField } from "bits-ui";
let value = $state({
start: new CalendarDateTime(2024, 8, 3, 12, 30),
end: new CalendarDateTime(2024, 8, 4, 12, 30),
});
</script>
<DateRangeField.Root
bind:value
onValueChange={(v) => {
value = {
start: v.start.set({ hour: v.start.hour + 1 }),
end: v.end.set({ hour: v.end.hour + 1 }),
};
}}
>
<!-- ... -->
</DateRangeField.Root>
Controlled
Sometimes, you may want complete control over the component's value
state, meaning you will be "kept in the loop" and be required to apply the state change yourself. While you will rarely need this, it's possible to do so by setting the controlledValue
prop to true
.
You will then be responsible for updating a local value state variable that is passed as the value
prop to the DateRangeField.Root
component.
<script lang="ts">
import { DateRangeField } from "bits-ui";
let myValue = $state();
</script>
<DateRangeField.Root controlledValue value={myValue} onValueChange={(v) => (myValue = v)}>
<!-- ... -->
</DateRangeField.Root>
See the Controlled State documentation for more information about controlled states.
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 |
controlledValue | boolean | Whether or not the value is controlled or not. If Default: false |
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 |
controlledPlaceholder | boolean | Whether or not the placeholder is controlled or not. If Default: false |
isDateUnavailable | function | A function that returns whether or not a date is unavailable. 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 delegation 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 delegation 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 delegation 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 delegation 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. |