Time Range Field

GitHubnpm
Allows users to input a range of times within a designated field.
Alpha
––
––
––
AM
-
––
––
––
AM
<script setup lang="ts">
import { Label, TimeRangeFieldInput, TimeRangeFieldRoot } from 'akar';
import './styles.css';
</script>

<template>
  <div class="akar-demo akar-demo-TimeRangeField">
    <div class="TimeFieldWrapper">
      <Label
        class="TimeFieldLabel"
        for="time-field"
      >Input label</Label>

      <TimeRangeFieldRoot
        id="time-field"
        v-slot="{ segments }"
        granularity="second"
        class="TimeField"
      >
        <template
          v-for="(item, index) in segments.start"
          :key="`${item.part}-${index}`"
        >
          <TimeRangeFieldInput
            v-if="item.part === 'literal'"
            :part="item.part"
            class="TimeFieldLiteral"
            type="start"
          >
            {{ item.value }}
          </TimeRangeFieldInput>
          <TimeRangeFieldInput
            v-else
            :part="item.part"
            class="TimeFieldSegment"
            type="start"
          >
            {{ item.value }}
          </TimeRangeFieldInput>
        </template>
        -
        <template
          v-for="(item, index) in segments.end"
          :key="`${item.part}-${index}`"
        >
          <TimeRangeFieldInput
            v-if="item.part === 'literal'"
            :part="item.part"
            class="TimeFieldLiteral"
            type="end"
          >
            {{ item.value }}
          </TimeRangeFieldInput>
          <TimeRangeFieldInput
            v-else
            :part="item.part"
            class="TimeFieldSegment"
            type="end"
          >
            {{ item.value }}
          </TimeRangeFieldInput>
        </template>
      </TimeRangeFieldRoot>
    </div>
  </div>
</template>

Features

  • Full keyboard navigation
  • Can be controlled or uncontrolled
  • Focus is fully managed
  • Localization support
  • Highly composable
  • Accessible by default
  • Supports various time formats
  • Time range validation
  • Configurable granularity

Preface

The component depends on the @internationalized/date package, which solves a lot of the problems that come with working with dates and times in JavaScript.

We highly recommend reading through the documentation for the package to get a solid feel for how it works, and you'll need to install it in your project to use the time-related components.

Installation

Install the date package.

pnpm add @internationalized/date

Install the component from your command line.

pnpm add akar

Anatomy

Import all parts and piece them together.

<script setup>
import {
  TimeRangeFieldInput,
  TimeRangeFieldRoot,
} from 'akar';
</script>

<template>
  <TimeRangeFieldRoot>
    <TimeRangeFieldInput
      part="hour"
      type="start"
    />
    <TimeRangeFieldInput
      part="minute"
      type="start"
    />
    <TimeRangeFieldInput
      part="hour"
      type="end"
    />
    <TimeRangeFieldInput
      part="minute"
      type="end"
    />
  </TimeRangeFieldRoot>
</template>

API Reference

Root

Contains all the parts of a time range field.

PropDefaultType
as
"div"
AsTag | Component

The element or component this component should render as. Can be overwritten by asChild.

asChild
boolean

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read our Composition guide for more details.

defaultPlaceholder
TimeValue

The default placeholder time

defaultValue
TimeRange

The default value for the field

dir
"ltr" | "rtl"

The reading direction of the time field when applicable.

If omitted, inherits globally from ConfigProvider or assumes LTR (left-to-right) reading mode.

disabled
false
boolean

Whether or not the time field is disabled

granularity
"hour" | "minute" | "second"

The granularity to use for formatting times. Defaults to minute. The field will render segments for each part of the time up to and including the specified granularity

hideTimeZone
boolean

Whether or not to hide the time zone segment of the field

hourCycle
12 | 24

The hour cycle used for formatting times. Defaults to the local preference

id
string

Id of the element

isTimeUnavailable
Matcher

A function that returns whether or not a time is unavailable

locale
string

The locale to use for formatting times

maxValue
TimeValue

The maximum time that can be selected

minValue
TimeValue

The minimum time that can be selected

modelValue
TimeRange | null

The controlled checked state of the field. Can be bound as v-model.

name
string

The name of the field. Submitted with its owning form as part of a name/value pair.

placeholder
TimeValue

The placeholder time, which is used to determine what time to display when no time is selected. This updates as the user navigates the field

readonly
false
boolean

Whether or not the time field is readonly

required
boolean

When true, indicates that the user must set the value before the owning form can be submitted.

step
DateStep

The stepping interval for the time fields. Defaults to 1.

EventPayload
update:modelValue
date: TimeRange

Event handler called whenever the model value changes

update:placeholder
date: TimeValue

Event handler called whenever the placeholder value changes

Slot (default)Payload
modelValue
TimeRange

The current time of the field

segments
{ start: { part: SegmentPart; value: string; }[]; end: { part: SegmentPart; value: string; }[]; }

The time field segment contents

isInvalid
boolean

Value if the input is invalid

MethodType
setFocusedElement
(el: HTMLElement) => void

Helper to set the focused element inside the TimeRangeField

TimeRangeFieldRoot.vue source

Data attribute Value
[data-readonly]
Present when readonly
[data-disabled]
Present when disabled
[data-invalid]
Present when invalid

Input

Contains the time field segments.

PropDefaultType
as
"div"
AsTag | Component

The element or component this component should render as. Can be overwritten by asChild.

asChild
boolean

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read our Composition guide for more details.

part*
"day" | "month" | "year" | "hour" | "minute" | "second" | "dayPeriod" | "literal" | "timeZoneName"

The part of the date to render

type*
"start" | "end"

The type of field to render (start or end)

TimeRangeFieldInput.vue source

Data attribute Value
[data-disabled]
Present when disabled
[data-invalid]
Present when invalid
[data-placeholder]
Present when no value is set

Accessibility

Keyboard Interactions

Key Description
Tab
When focus moves onto the time field, focuses the first segment.
ArrowLeft/ArrowRight
Navigates between the time field segments.
ArrowUp/ArrowDown
Increments/changes the value of the segment.
0-9
When the focus is on a numeric TimeRangeFieldInput, it types in the number and focuses the next segment if the next input would result in an invalid value.
Backspace
Deletes a digit from the focused numeric segments.
A/P
When the focus is on the day period, it sets it to AM or PM.

Usage Examples

Basic Usage

<script setup>
import { Time } from '@internationalized/date';
import { TimeRangeFieldInput, TimeRangeFieldRoot } from 'akar';
import { ref } from 'vue';

const timeRange = ref({
  start: new Time(9, 0),
  end: new Time(17, 0)
});
</script>

<template>
  <TimeRangeFieldRoot v-model="timeRange">
    <div class="flex gap-2 items-center">
      <TimeRangeFieldInput
        part="hour"
        type="start"
      />
      :
      <TimeRangeFieldInput
        part="minute"
        type="start"
      />
      <span class="mx-2">to
        <TimeRangeFieldInput
          part="hour"
          type="end"
        />
        :
        <TimeRangeFieldInput
          part="minute"
          type="end"
        />
      </span>
    </div>
  </TimeRangeFieldRoot>
</template>

Controlled Component

<script setup>
import { TimeRangeFieldInput, TimeRangeFieldRoot } from 'akar';
import { ref } from 'vue';

const timeRange = ref({
  start: undefined,
  end: undefined
});

function handleTimeRangeChange(newRange) {
  console.log('Time range changed:', newRange);
  timeRange.value = newRange;
}
</script>

<template>
  <TimeRangeFieldRoot
    :model-value="timeRange"
    @update:model-value="handleTimeRangeChange"
  >
    <div class="flex gap-2 items-center">
      <TimeRangeFieldInput
        part="hour"
        type="start"
      />
      :
      <TimeRangeFieldInput
        part="minute"
        type="start"
      />
      <span class="mx-2">to
        <TimeRangeFieldInput
          part="hour"
          type="end"
        />
        :
        <TimeRangeFieldInput
          part="minute"
          type="end"
        />
      </span>
    </div>
  </TimeRangeFieldRoot>
</template>

With Validation

<script setup>
import { Time } from '@internationalized/date';
import { TimeRangeFieldInput, TimeRangeFieldRoot } from 'akar';
import { ref } from 'vue';

const timeRange = ref({
  start: new Time(9, 0),
  end: new Time(17, 0)
});

// Disable times before 8 AM and after 6 PM
function isTimeUnavailable(time) {
  const hour = time.hour;
  return hour < 8 || hour > 18;
}
</script>

<template>
  <TimeRangeFieldRoot
    v-model="timeRange"
    :is-time-unavailable="isTimeUnavailable"
  >
    <div class="flex gap-2 items-center">
      <TimeRangeFieldInput
        part="hour"
        type="start"
      />
      :
      <TimeRangeFieldInput
        part="minute"
        type="start"
      />
      <span class="mx-2">to
        <TimeRangeFieldInput
          part="hour"
          type="end"
        />
        :
        <TimeRangeFieldInput
          part="minute"
          type="end"
        />
      </span>
    </div>
    <p
      v-if="timeRange.start && isTimeUnavailable(timeRange.start)"
      class="text-sm color-red-500 mt-1"
    >
      Start time is unavailable
    </p>
    <p
      v-if="timeRange.end && isTimeUnavailable(timeRange.end)"
      class="text-sm color-red-500 mt-1"
    >
      End time is unavailable
    </p>
  </TimeRangeFieldRoot>
</template>

With Different Granularity

<script setup>
import { Time } from '@internationalized/date';
import { TimeRangeFieldInput, TimeRangeFieldRoot } from 'akar';
import { ref } from 'vue';

const timeRange = ref({
  start: new Time(9, 0, 30),
  end: new Time(17, 30, 0)
});
</script>

<template>
  <div class="space-y-4">
    <!-- Hour and minute only -->
    <div>
      <label class="text-sm font-medium mb-2 block">Hour and Minute</label>
      <TimeRangeFieldRoot
        v-model="timeRange"
        granularity="minute"
      >
        <div class="flex gap-2 items-center">
          <TimeRangeFieldInput
            part="hour"
            type="start"
          />
          :
          <TimeRangeFieldInput
            part="minute"
            type="start"
          />
          <span class="mx-2">to
            <TimeRangeFieldInput
              part="hour"
              type="end"
            />
            :
            <TimeRangeFieldInput
              part="minute"
              type="end"
            />
          </span>
        </div>
      </TimeRangeFieldRoot>
    </div>

    <!-- Hour only -->
    <div>
      <label class="text-sm font-medium mb-2 block">Hour Only</label>
      <TimeRangeFieldRoot
        v-model="timeRange"
        granularity="hour"
      >
        <div class="flex gap-2 items-center">
          <TimeRangeFieldInput
            part="hour"
            type="start"
          />
          <span class="mx-2">to
            <TimeRangeFieldInput
              part="hour"
              type="end"
            />
          </span>
        </div>
      </TimeRangeFieldRoot>
    </div>

    <!-- Hour, minute, and second -->
    <div>
      <label class="text-sm font-medium mb-2 block">Hour, Minute, and Second</label>
      <TimeRangeFieldRoot
        v-model="timeRange"
        granularity="second"
      >
        <div class="flex gap-2 items-center">
          <TimeRangeFieldInput
            part="hour"
            type="start"
          />
          :
          <TimeRangeFieldInput
            part="minute"
            type="start"
          />
          :
          <TimeRangeFieldInput
            part="second"
            type="start"
          />
          <span class="mx-2">to
            <TimeRangeFieldInput
              part="hour"
              type="end"
            />
            :
            <TimeRangeFieldInput
              part="minute"
              type="end"
            />
            :
            <TimeRangeFieldInput
              part="second"
              type="end"
            />
          </span>
        </div>
      </TimeRangeFieldRoot>
    </div>
  </div>
</template>

With Locale and Hour Cycle

<script setup>
import { Time } from '@internationalized/date';
import { TimeRangeFieldInput, TimeRangeFieldRoot } from 'akar';
import { ref } from 'vue';

const timeRange = ref({
  start: new Time(9, 0),
  end: new Time(17, 0)
});
</script>

<template>
  <div class="space-y-4">
    <!-- 24-hour format (default) -->
    <div>
      <label class="text-sm font-medium mb-2 block">24-hour format</label>
      <TimeRangeFieldRoot
        v-model="timeRange"
        hour-cycle="h23"
      >
        <div class="flex gap-2 items-center">
          <TimeRangeFieldInput
            part="hour"
            type="start"
          />
          :
          <TimeRangeFieldInput
            part="minute"
            type="start"
          />
          <span class="mx-2">to
            <TimeRangeFieldInput
              part="hour"
              type="end"
            />
            :
            <TimeRangeFieldInput
              part="minute"
              type="end"
            />
          </span>
        </div>
      </TimeRangeFieldRoot>
    </div>

    <!-- 12-hour format with AM/PM -->
    <div>
      <label class="text-sm font-medium mb-2 block">12-hour format</label>
      <TimeRangeFieldRoot
        v-model="timeRange"
        hour-cycle="h12"
      >
        <div class="flex gap-2 items-center">
          <TimeRangeFieldInput
            part="hour"
            type="start"
          />
          :
          <TimeRangeFieldInput
            part="minute"
            type="start"
          />
          <TimeRangeFieldInput
            part="dayPeriod"
            type="start"
          />
          <span class="mx-2">to
            <TimeRangeFieldInput
              part="hour"
              type="end"
            />
            :
            <TimeRangeFieldInput
              part="minute"
              type="end"
            />
            <TimeRangeFieldInput
              part="dayPeriod"
              type="end"
            />
          </span>
        </div>
      </TimeRangeFieldRoot>
    </div>

    <!-- French locale -->
    <div>
      <label class="text-sm font-medium mb-2 block">French locale</label>
      <TimeRangeFieldRoot
        v-model="timeRange"
        locale="fr"
      >
        <div class="flex gap-2 items-center">
          <TimeRangeFieldInput
            part="hour"
            type="start"
          />
          :
          <TimeRangeFieldInput
            part="minute"
            type="start"
          />
          <span class="mx-2">à
            <TimeRangeFieldInput
              part="hour"
              type="end"
            />
            :
            <TimeRangeFieldInput
              part="minute"
              type="end"
            />
          </span>
        </div>
      </TimeRangeFieldRoot>
    </div>
  </div>
</template>

With Min and Max Values

<script setup>
import { Time } from '@internationalized/date';
import { TimeRangeFieldInput, TimeRangeFieldRoot } from 'akar';
import { ref } from 'vue';

const timeRange = ref({
  start: new Time(9, 0),
  end: new Time(17, 0)
});

// Restrict times between 8 AM and 6 PM
const minTime = new Time(8, 0);
const maxTime = new Time(18, 0);
</script>

<template>
  <TimeRangeFieldRoot
    v-model="timeRange"
    :min-value="minTime"
    :max-value="maxTime"
  >
    <div class="flex gap-2 items-center">
      <TimeRangeFieldInput
        part="hour"
        type="start"
      />
      :
      <TimeRangeFieldInput
        part="minute"
        type="start"
      />
      <span class="mx-2">to
        <TimeRangeFieldInput
          part="hour"
          type="end"
        />
        :
        <TimeRangeFieldInput
          part="minute"
          type="end"
        />
      </span>
    </div>
    <p class="text-sm color-gray-500 mt-1">
      Business hours: 8:00 AM - 6:00 PM
    </p>
  </TimeRangeFieldRoot>
</template>

With Step Increment

<script setup>
import { Time } from '@internationalized/date';
import { TimeRangeFieldInput, TimeRangeFieldRoot } from 'akar';
import { ref } from 'vue';

const timeRange = ref({
  start: new Time(9, 0),
  end: new Time(17, 0)
});

// Increment minutes by 15
const step = { minute: 15 };
</script>

<template>
  <TimeRangeFieldRoot
    v-model="timeRange"
    :step="step"
  >
    <div class="flex gap-2 items-center">
      <TimeRangeFieldInput
        part="hour"
        type="start"
      />
      :
      <TimeRangeFieldInput
        part="minute"
        type="start"
      />
      <span class="mx-2">to
        <TimeRangeFieldInput
          part="hour"
          type="end"
        />
        :
        <TimeRangeFieldInput
          part="minute"
          type="end"
        />
      </span>
    </div>
    <p class="text-sm color-gray-500 mt-1">
      Minutes increment by 15
    </p>
  </TimeRangeFieldRoot>
</template>

Disabled State

<script setup>
import { Time } from '@internationalized/date';
import { TimeRangeFieldInput, TimeRangeFieldRoot } from 'akar';
import { ref } from 'vue';

const timeRange = ref({
  start: new Time(9, 0),
  end: new Time(17, 0)
});

const isDisabled = ref(true);
</script>

<template>
  <div class="space-y-4">
    <div class="flex gap-2 items-center">
      <input
        id="disable"
        v-model="isDisabled"
        type="checkbox"
      >
      <label for="disable">Disable time range field</label>
    </div>

    <TimeRangeFieldRoot
      v-model="timeRange"
      :disabled="isDisabled"
    >
      <div class="flex gap-2 items-center">
        <TimeRangeFieldInput
          part="hour"
          type="start"
        />
        :
        <TimeRangeFieldInput
          part="minute"
          type="start"
        />
        <span class="mx-2">to
          <TimeRangeFieldInput
            part="hour"
            type="end"
          />
          :
          <TimeRangeFieldInput
            part="minute"
            type="end"
          />
        </span>
      </div>
    </TimeRangeFieldRoot>
  </div>
</template>

Read-only State

<script setup>
import { Time } from '@internationalized/date';
import { TimeRangeFieldInput, TimeRangeFieldRoot } from 'akar';
import { ref } from 'vue';

const timeRange = ref({
  start: new Time(9, 0),
  end: new Time(17, 0)
});

const isReadonly = ref(true);
</script>

<template>
  <div class="space-y-4">
    <div class="flex gap-2 items-center">
      <input
        id="readonly"
        v-model="isReadonly"
        type="checkbox"
      >
      <label for="readonly">Make time range field read-only</label>
    </div>

    <TimeRangeFieldRoot
      v-model="timeRange"
      :readonly="isReadonly"
    >
      <div class="flex gap-2 items-center">
        <TimeRangeFieldInput
          part="hour"
          type="start"
        />
        :
        <TimeRangeFieldInput
          part="minute"
          type="start"
        />
        <span class="mx-2">to
          <TimeRangeFieldInput
            part="hour"
            type="end"
          />
          :
          <TimeRangeFieldInput
            part="minute"
            type="end"
          />
        </span>
      </div>
    </TimeRangeFieldRoot>
  </div>
</template>

Advanced Keyboard Navigation

The TimeRangeField provides intuitive keyboard navigation for efficient time input. Users can seamlessly move between time segments, increment or decrement values, and type numbers directly.

<script setup>
import { Time } from '@internationalized/date';
import { TimeRangeFieldInput, TimeRangeFieldRoot } from 'akar';
import { ref } from 'vue';

const timeRange = ref({
  start: new Time(9, 0),
  end: new Time(17, 0)
});
</script>

<template>
  <div class="space-y-4">
    <p class="text-sm color-gray-600">
      Try navigating with Tab, Arrow keys, and typing numbers. Focus moves automatically between segments.
    </p>
    <TimeRangeFieldRoot v-model="timeRange">
      <div class="flex gap-2 items-center">
        <TimeRangeFieldInput
          part="hour"
          type="start"
        />
        :
        <TimeRangeFieldInput
          part="minute"
          type="start"
        />
        <span class="mx-2">to
          <TimeRangeFieldInput
            part="hour"
            type="end"
          />
          :
          <TimeRangeFieldInput
            part="minute"
            type="end"
          />
        </span>
      </div>
    </TimeRangeFieldRoot>
    <div class="text-xs color-gray-500">
      <strong>Keyboard shortcuts:</strong> Tab to navigate, Arrow Up/Down to change values, type numbers to input.
    </div>
  </div>
</template>

Form Integration

Integrate TimeRangeField with HTML forms to handle submissions and validation.

<script setup>
import { Time } from '@internationalized/date';
import { TimeRangeFieldInput, TimeRangeFieldRoot } from 'akar';
import { ref } from 'vue';

const timeRange = ref({
  start: new Time(9, 0),
  end: new Time(17, 0)
});

function handleSubmit() {
  console.log('Form submitted with time range:', timeRange.value);
  // Handle form submission
}
</script>

<template>
  <form
    class="space-y-4"
    @submit.prevent="handleSubmit"
  >
    <div>
      <label class="text-sm font-medium mb-2 block">Select Time Range</label>
      <TimeRangeFieldRoot v-model="timeRange">
        <div class="flex gap-2 items-center">
          <TimeRangeFieldInput
            part="hour"
            type="start"
          />
          :
          <TimeRangeFieldInput
            part="minute"
            type="start"
          />
          <span class="mx-2">to
            <TimeRangeFieldInput
              part="hour"
              type="end"
            />
            :
            <TimeRangeFieldInput
              part="minute"
              type="end"
            />
          </span>
        </div>
      </TimeRangeFieldRoot>
    </div>
    <button
      type="submit"
      class="color-white px-4 py-2 rounded bg-blue-500"
    >
      Submit
    </button>
  </form>
</template>

Custom Styling

Customize the appearance of the TimeRangeField using CSS classes or Tailwind utilities.

<script setup>
import { Time } from '@internationalized/date';
import { TimeRangeFieldInput, TimeRangeFieldRoot } from 'akar';
import { ref } from 'vue';

const timeRange = ref({
  start: new Time(9, 0),
  end: new Time(17, 0)
});
</script>

<template>
  <TimeRangeFieldRoot v-model="timeRange">
    <div class="p-4 border border-gray-300 rounded-lg bg-gray-50 flex gap-2 items-center">
      <TimeRangeFieldInput
        part="hour"
        type="start"
        class="px-2 py-1 text-center border border-blue-300 rounded w-12 focus:outline-none focus:border-blue-500"
      />
      <span class="color-gray-500">:
        <TimeRangeFieldInput
          part="minute"
          type="start"
          class="px-2 py-1 text-center border border-blue-300 rounded w-12 focus:outline-none focus:border-blue-500"
        />
        <span class="color-gray-500 mx-2">to
          <TimeRangeFieldInput
            part="hour"
            type="end"
            class="px-2 py-1 text-center border border-blue-300 rounded w-12 focus:outline-none focus:border-blue-500"
          />
          <span class="color-gray-500">:
            <TimeRangeFieldInput
              part="minute"
              type="end"
              class="px-2 py-1 text-center border border-blue-300 rounded w-12 focus:outline-none focus:border-blue-500"
            />
          </span></span></span>
    </div>
  </TimeRangeFieldRoot>
</template>

Advanced Validation

Implement complex validation rules, such as ensuring the end time is after the start time and within business hours.

<script setup>
import { Time } from '@internationalized/date';
import { TimeRangeFieldInput, TimeRangeFieldRoot } from 'akar';
import { ref, watch } from 'vue';

const timeRange = ref({
  start: new Time(9, 0),
  end: new Time(17, 0)
});

const errors = ref([]);

watch(timeRange, (newRange) => {
  errors.value = [];
  if (newRange.start && newRange.end) {
    if (newRange.start.compare(newRange.end) >= 0) {
      errors.value.push('End time must be after start time');
    }
    if (newRange.start.hour < 8 || newRange.start.hour > 18) {
      errors.value.push('Start time must be between 8 AM and 6 PM');
    }
    if (newRange.end.hour < 8 || newRange.end.hour > 18) {
      errors.value.push('End time must be between 8 AM and 6 PM');
    }
  }
}, { deep: true });
</script>

<template>
  <TimeRangeFieldRoot v-model="timeRange">
    <div class="flex gap-2 items-center">
      <TimeRangeFieldInput
        part="hour"
        type="start"
      />
      :
      <TimeRangeFieldInput
        part="minute"
        type="start"
      />
      <span class="mx-2">to
        <TimeRangeFieldInput
          part="hour"
          type="end"
        />
        :
        <TimeRangeFieldInput
          part="minute"
          type="end"
        />
      </span>
    </div>
    <div
      v-if="errors.length"
      class="mt-2"
    >
      <p
        v-for="error in errors"
        :key="error"
        class="text-sm color-red-500"
      >
        {{ error }}
      </p>
    </div>
  </TimeRangeFieldRoot>
</template>

Accessibility Features

The TimeRangeField is built with accessibility in mind. Enhance it further with ARIA labels and descriptions for screen readers.

<script setup>
import { Time } from '@internationalized/date';
import { TimeRangeFieldInput, TimeRangeFieldRoot } from 'akar';
import { ref } from 'vue';

const timeRange = ref({
  start: new Time(9, 0),
  end: new Time(17, 0)
});
</script>

<template>
  <div>
    <label
      for="time-range"
      class="text-sm font-medium mb-2 block"
    >Meeting Time Range</label>
    <TimeRangeFieldRoot
      id="time-range"
      v-model="timeRange"
      aria-describedby="time-range-help"
    >
      <div class="flex gap-2 items-center">
        <TimeRangeFieldInput
          part="hour"
          type="start"
          aria-label="Start hour"
        />
        <span aria-hidden="true">:
          <TimeRangeFieldInput
            part="minute"
            type="start"
            aria-label="Start minute"
          />
          <span
            class="mx-2"
            aria-hidden="true"
          >to
            <TimeRangeFieldInput
              part="hour"
              type="end"
              aria-label="End hour"
            />
            <span aria-hidden="true">:
              <TimeRangeFieldInput
                part="minute"
                type="end"
                aria-label="End minute"
              />
            </span></span></span>
      </div>
    </TimeRangeFieldRoot>
    <p
      id="time-range-help"
      class="text-xs color-gray-500 mt-1"
    >
      Select the start and end times for your meeting. Use Tab to navigate between fields.
    </p>
  </div>
</template>

Real-world Use Cases

Use TimeRangeField in practical scenarios like scheduling appointments or booking resources.

<script setup>
import { Time } from '@internationalized/date';
import { TimeRangeFieldInput, TimeRangeFieldRoot } from 'akar';
import { ref } from 'vue';

const appointment = ref({
  date: new Date(),
  timeRange: {
    start: new Time(10, 0),
    end: new Time(11, 0)
  },
  title: ''
});

function bookAppointment() {
  console.log('Booking appointment:', appointment.value);
  // API call to book appointment
}
</script>

<template>
  <div class="mx-auto p-6 rounded-lg bg-white max-w-md shadow-lg">
    <h2 class="text-xl font-bold mb-4">
      Book an Appointment
    </h2>
    <form
      class="space-y-4"
      @submit.prevent="bookAppointment"
    >
      <div>
        <label class="text-sm font-medium mb-2 block">Appointment Title</label>
        <input
          v-model="appointment.title"
          type="text"
          class="px-3 py-2 border border-gray-300 rounded w-full"
          required
        >
      </div>
      <div>
        <label class="text-sm font-medium mb-2 block">Date</label>
        <input
          v-model="appointment.date"
          type="date"
          class="px-3 py-2 border border-gray-300 rounded w-full"
          required
        >
      </div>
      <div>
        <label class="text-sm font-medium mb-2 block">Time Range</label>
        <TimeRangeFieldRoot v-model="appointment.timeRange">
          <div class="flex gap-2 items-center">
            <TimeRangeFieldInput
              part="hour"
              type="start"
              class="px-2 py-1 text-center border border-gray-300 rounded w-12"
            />
            :
            <TimeRangeFieldInput
              part="minute"
              type="start"
              class="px-2 py-1 text-center border border-gray-300 rounded w-12"
            />
            <span class="mx-2">to
              <TimeRangeFieldInput
                part="hour"
                type="end"
                class="px-2 py-1 text-center border border-gray-300 rounded w-12"
              />
              :
              <TimeRangeFieldInput
                part="minute"
                type="end"
                class="px-2 py-1 text-center border border-gray-300 rounded w-12"
              />
            </span>
          </div>
        </TimeRangeFieldRoot>
      </div>
      <button
        type="submit"
        class="color-white px-4 py-2 rounded bg-green-500 w-full hover:bg-green-600"
      >
        Book Appointment
      </button>
    </form>
  </div>
</template>