Time Range Field
Enables users to input a range of times within a designated field.
	<script lang="ts">
  import { TimeRangeField } from "bits-ui";
</script>
 
<TimeRangeField.Root class="group flex w-full max-w-[320px] flex-col gap-1.5">
  <TimeRangeField.Label class="block select-none text-sm font-medium">
    Hotel dates
  </TimeRangeField.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 (type)}
      <TimeRangeField.Input {type}>
        {#snippet children({ segments })}
          {#each segments as { part, value }, i (part + i)}
            <div class="inline-block select-none">
              {#if part === "literal"}
                <TimeRangeField.Segment
                  {part}
                  class="text-muted-foreground p-1"
                >
                  {value}
                </TimeRangeField.Segment>
              {:else}
                <TimeRangeField.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}
                </TimeRangeField.Segment>
              {/if}
            </div>
          {/each}
        {/snippet}
      </TimeRangeField.Input>
      {#if type === "start"}
        <div aria-hidden="true" class="text-muted-foreground pl-1 pr-2">to</div>
      {/if}
    {/each}
  </div>
</TimeRangeField.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/Times documentation to learn more!
Overview
The TimeRangeField component combines two Time Field components to create a time range field. Check out the Time Field component documentation for information on how to customize this component.
Structure
	<script lang="ts">
  import { TimeRangeField } from "bits-ui";
</script>
 
<TimeRangeField.Root>
  <TimeRangeField.Label>Working Hours</TimeRangeField.Label>
  {#each ["start", "end"] as const as type}
    <TimeRangeField.Input {type}>
      {#snippet children({ segments })}
        {#each segments as { part, value }}
          <TimeRangeField.Segment {part}>
            {value}
          </TimeRangeField.Segment>
        {/each}
      {/snippet}
    </TimeRangeField.Input>
  {/each}
</TimeRangeField.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 { TimeRangeField } from "bits-ui";
  import { Time } from "@internationalized/date";
  let myPlaceholder = $state(new Time(12, 30));
</script>
 
<TimeRangeField.Root bind:placeholder={myPlaceholder}>
  <!-- ... -->
</TimeRangeField.Root>
Fully Controlled
Use a Function Binding for complete control over the state's reads and writes.
	<script lang="ts">
  import { TimeRangeField, type TimeValue } from "bits-ui";
  import { Time } from "@internationalized/date";
  let myPlaceholder = $state(new Time(12, 30));
 
  function getPlaceholder() {
    return myPlaceholder;
  }
 
  function setPlaceholder(newPlaceholder: TimeValue) {
    myPlaceholder = newPlaceholder;
  }
</script>
 
<TimeRangeField.Root bind:placeholder={getPlaceholder, setPlaceholder}>
  <!-- ... -->
</TimeRangeField.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 { TimeRangeField, type TimeRange } from "bits-ui";
  import { Time } from "@internationalized/date";
  let myValue = $state<TimeRange>({
    start: new Time(12, 30),
    end: new Time(12, 30),
  });
</script>
 
<button
  onclick={() => {
    myValue = {
      start: myValue.start.add({ hours: 1 }),
      end: myValue.end.add({ hours: 1 }),
    };
  }}
>
  Add 1 hour
</button>
<TimeRangeField.Root bind:value={myValue}>
  <!-- ... -->
</TimeRangeField.Root>
Fully Controlled
Use a Function Binding for complete control over the state's reads and writes.
	<script lang="ts">
  import { TimeRangeField, type TimeRange } from "bits-ui";
 
  let myValue = $state<TimeRange | undefined>({
    start: undefined,
    end: undefined,
  });
 
  function getValue() {
    return myValue;
  }
 
  function setValue(newValue: TimeRange | undefined) {
    myValue = newValue;
  }
</script>
 
<DateRangeField.Root bind:value={getValue, setValue}>
  <!-- ... -->
</DateRangeField.Root>
API Reference
The root time field component.
| Property | Details | 
|---|---|
| value | |
| onValueChange | |
| placeholder | |
| onPlaceholderChange | |
| errorMessageId | |
| validate | |
| onInvalid | |
| minValue | |
| maxValue | |
| granularity | |
| hideTimeZone | |
| hourCycle | |
| locale | |
| disabled | |
| readonly | |
| readonlySegments | |
| required | |
| onStartValueChange | |
| onEndValueChange | |
| ref | |
| children | |
| child | 
| Data Attribute | Details | 
|---|---|
| data-time-range-field-root | 
The container for the segments of the time field.
| Property | Details | 
|---|---|
| type | |
| name | |
| ref | |
| children | |
| child | 
| Data Attribute | Details | 
|---|---|
| data-invalid | |
| data-disabled | |
| data-time-field-input | 
A segment of the time field.
| Property | Details | 
|---|---|
| part | |
| ref | |
| children | |
| child | 
| Data Attribute | Details | 
|---|---|
| data-invalid | |
| data-disabled | |
| data-segment | |
| data-time-field-segment | 
The label for the time field.
| Property | Details | 
|---|---|
| ref | |
| children | |
| child | 
| Data Attribute | Details | 
|---|---|
| data-invalid | |
| data-time-field-label |