AvesraAvesrabeta

TimeField

Segmented time input with label, description, and validation, using @internationalized/date. Reuses av-date-input-group.

Usage

import { AvTimeFieldImports } from '@avesra/angular';
import { Component } from '@angular/core';
import {
  AvTimeFieldImports,
  AvLabelComponent,
} from '@avesra/angular';

@Component({
  selector: 'app-time-field-basic-demo',
  imports: [
    AvTimeFieldImports,
    AvLabelComponent,
  ],
  template: `<div av-time-field class="w-[256px]" name="time">
  <label av-label>Time</label>
  <div av-date-input-group>
    <div av-date-input-group-input></div>
  </div>
</div>`,
})
export class TimeFieldBasicDemo {}

Anatomy

<div av-time-field>
  <label av-label></label>
  <div av-date-input-group>
    <div av-date-input-group-input></div>
  </div>
  <p av-description></p>
  <p av-field-error></p>
</div>

TimeField combines label, segmented time input, description, and error into one accessible field. Compose shared av-label, av-description, and av-field-error parts; the input auto-renders locale-ordered time segments via av-date-input-group.

Examples

With Icons

Add prefix or suffix icons to enhance the time field.

import { Component } from '@angular/core';
import {
  AvTimeFieldImports,
  AvLabelComponent,
} from '@avesra/angular';
import { AppIconComponent } from '../../components/app-icon/app-icon.component';

@Component({
  selector: 'app-time-field-with-prefix-icon-demo',
  imports: [
    AvTimeFieldImports,
    AvLabelComponent,
    AppIconComponent,
  ],
  template: `<div av-time-field class="w-[256px]" name="time">
  <label av-label>Time</label>
  <div av-date-input-group>
    <div av-date-input-group-prefix>
      <app-icon icon="solar:clock-circle-linear" size="16" class="text-muted" />
    </div>
    <div av-date-input-group-input></div>
  </div>
</div>`,
})
export class TimeFieldWithPrefixIconDemo {}
import { Component } from '@angular/core';
import {
  AvTimeFieldImports,
  AvLabelComponent,
} from '@avesra/angular';
import { AppIconComponent } from '../../components/app-icon/app-icon.component';

@Component({
  selector: 'app-time-field-with-suffix-icon-demo',
  imports: [
    AvTimeFieldImports,
    AvLabelComponent,
    AppIconComponent,
  ],
  template: `<div av-time-field class="w-[256px]" name="time">
  <label av-label>Time</label>
  <div av-date-input-group>
    <div av-date-input-group-input></div>
    <div av-date-input-group-suffix>
      <app-icon icon="solar:clock-circle-linear" size="16" class="text-muted" />
    </div>
  </div>
</div>`,
})
export class TimeFieldWithSuffixIconDemo {}
import { Component } from '@angular/core';
import {
  AvTimeFieldImports,
  AvLabelComponent,
} from '@avesra/angular';
import { AppIconComponent } from '../../components/app-icon/app-icon.component';

@Component({
  selector: 'app-time-field-with-prefix-and-suffix-demo',
  imports: [
    AvTimeFieldImports,
    AvLabelComponent,
    AppIconComponent,
  ],
  template: `<div av-time-field class="w-[256px]" name="time">
  <label av-label>Time</label>
  <div av-date-input-group>
    <div av-date-input-group-prefix>
      <app-icon icon="solar:clock-circle-linear" size="16" class="text-muted" />
    </div>
    <div av-date-input-group-input></div>
    <div av-date-input-group-suffix>
      <app-icon icon="solar:alt-arrow-down-linear" size="16" class="text-muted" />
    </div>
  </div>
</div>`,
})
export class TimeFieldWithPrefixAndSuffixDemo {}

Variants

The date input group supports two visual variants:

  • primary (default) — standard styling with shadow, suitable for most use cases
  • secondary — lower emphasis without shadow, suitable inside Surface
import { Component } from '@angular/core';
import {
  AvTimeFieldImports,
  AvLabelComponent,
} from '@avesra/angular';

@Component({
  selector: 'app-time-field-variants-demo',
  imports: [
    AvTimeFieldImports,
    AvLabelComponent,
  ],
  template: `<div class="flex flex-col gap-4">
  <div av-time-field class="w-[256px]" name="primary-time">
    <label av-label>Primary variant</label>
    <div av-date-input-group variant="primary">
      <div av-date-input-group-input></div>
    </div>
  </div>
  <div av-time-field class="w-[256px]" name="secondary-time">
    <label av-label>Secondary variant</label>
    <div av-date-input-group variant="secondary">
      <div av-date-input-group-input></div>
    </div>
  </div>
</div>`,
})
export class TimeFieldVariantsDemo {}

In Surface

When used inside a Surface component, use variant="secondary" on av-date-input-group to apply the lower emphasis variant suitable for surface backgrounds.

Enter a time

Enter a time for your appointment

import { Component } from '@angular/core';
import {
  AvTimeFieldImports,
  AvDescriptionComponent,
  AvLabelComponent,
  AvSurfaceComponent,
} from '@avesra/angular';
import { AppIconComponent } from '../../components/app-icon/app-icon.component';

@Component({
  selector: 'app-time-field-on-surface-demo',
  imports: [
    AvSurfaceComponent,
    AvTimeFieldImports,
    AvLabelComponent,
    AvDescriptionComponent,
    AppIconComponent,
  ],
  host: { class: 'w-full max-w-sm' },
  template: `<div av-surface class="flex w-full flex-col gap-4 rounded-3xl p-6">
  <div av-time-field class="w-full" name="time">
    <label av-label>Time</label>
    <div av-date-input-group variant="secondary">
      <div av-date-input-group-input></div>
    </div>
    <p av-description>Enter a time</p>
  </div>

  <div av-time-field class="w-full" name="time-2">
    <label av-label>Appointment time</label>
    <div av-date-input-group variant="secondary">
      <div av-date-input-group-prefix>
        <app-icon icon="solar:clock-circle-linear" size="16" class="text-muted" />
      </div>
      <div av-date-input-group-input></div>
    </div>
    <p av-description>Enter a time for your appointment</p>
  </div>
</div>`,
})
export class TimeFieldOnSurfaceDemo {}

With Description

Enter the start time

import { Component } from '@angular/core';
import {
  AvTimeFieldImports,
  AvDescriptionComponent,
  AvLabelComponent,
} from '@avesra/angular';

@Component({
  selector: 'app-time-field-with-description-demo',
  imports: [
    AvTimeFieldImports,
    AvLabelComponent,
    AvDescriptionComponent,
  ],
  template: `<div av-time-field class="w-[256px]" name="time">
  <label av-label>Time</label>
  <div av-date-input-group>
    <div av-date-input-group-input></div>
  </div>
  <p av-description>Enter the start time</p>
</div>`,
})
export class TimeFieldWithDescriptionDemo {}

Required Field

import { Component } from '@angular/core';
import {
  AvTimeFieldImports,
  AvLabelComponent,
} from '@avesra/angular';

@Component({
  selector: 'app-time-field-required-demo',
  imports: [
    AvTimeFieldImports,
    AvLabelComponent,
  ],
  template: `<div av-time-field class="w-[256px]" name="time" required>
  <label av-label required>Time</label>
  <div av-date-input-group>
    <div av-date-input-group-input></div>
  </div>
</div>`,
})
export class TimeFieldRequiredDemo {}

Disabled State

import { Component } from '@angular/core';
import {
  AvTimeFieldImports,
  AvLabelComponent,
} from '@avesra/angular';

@Component({
  selector: 'app-time-field-disabled-demo',
  imports: [
    AvTimeFieldImports,
    AvLabelComponent,
  ],
  template: `<div av-time-field class="w-[256px]" name="time" disabled>
  <label av-label disabled>Time</label>
  <div av-date-input-group disabled>
    <div av-date-input-group-input></div>
  </div>
</div>`,
})
export class TimeFieldDisabledDemo {}

Full Width

import { Component } from '@angular/core';
import {
  AvTimeFieldImports,
  AvLabelComponent,
} from '@avesra/angular';
import { AppIconComponent } from '../../components/app-icon/app-icon.component';

@Component({
  selector: 'app-time-field-full-width-demo',
  imports: [
    AvTimeFieldImports,
    AvLabelComponent,
    AppIconComponent,
  ],
  template: `<div class="w-[400px] space-y-4">
  <div av-time-field full-width name="time">
    <label av-label>Time</label>
    <div av-date-input-group>
      <div av-date-input-group-input></div>
    </div>
  </div>

  <div av-time-field full-width name="time-icons">
    <label av-label>Time</label>
    <div av-date-input-group>
      <div av-date-input-group-prefix>
        <app-icon icon="solar:clock-circle-linear" size="16" class="text-muted" />
      </div>
      <div av-date-input-group-input></div>
      <div av-date-input-group-suffix>
        <app-icon icon="solar:alt-arrow-down-linear" size="16" class="text-muted" />
      </div>
    </div>
  </div>
</div>`,
})
export class TimeFieldFullWidthDemo {}

Hour Cycle

Force a 12- or 24-hour clock with hour-cycle. The 24-hour field omits the AM/PM segment.

Uses AM/PM

No AM/PM segment

import { Component } from '@angular/core';
import { parseTime } from '@internationalized/date';
import {
  AvTimeFieldImports,
  AvDescriptionComponent,
  AvLabelComponent,
} from '@avesra/angular';

@Component({
  selector: 'app-time-field-hour-cycle-demo',
  imports: [
    AvTimeFieldImports,
    AvLabelComponent,
    AvDescriptionComponent,
  ],
  template: `<div class="flex flex-col gap-4 sm:flex-row">
  <div av-time-field class="w-[256px]" name="time-12" [hour-cycle]="12" [default-value]="value">
    <label av-label>12-hour</label>
    <div av-date-input-group>
      <div av-date-input-group-input></div>
    </div>
    <p av-description>Uses AM/PM</p>
  </div>

  <div av-time-field class="w-[256px]" name="time-24" [hour-cycle]="24" [default-value]="value">
    <label av-label>24-hour</label>
    <div av-date-input-group>
      <div av-date-input-group-input></div>
    </div>
    <p av-description>No AM/PM segment</p>
  </div>
</div>`,
})
export class TimeFieldHourCycleDemo {
  readonly value = parseTime('15:30');
}

Validation

Use invalid together with av-field-error to surface validation messages.

import { Component } from '@angular/core';
import {
  AvTimeFieldImports,
  AvFieldErrorComponent,
  AvLabelComponent,
} from '@avesra/angular';

@Component({
  selector: 'app-time-field-invalid-demo',
  imports: [
    AvTimeFieldImports,
    AvLabelComponent,
    AvFieldErrorComponent,
  ],
  template: `<div av-time-field class="w-[256px]" name="time" invalid required>
  <label av-label required invalid>Time</label>
  <div av-date-input-group invalid>
    <div av-date-input-group-input></div>
  </div>
  <p av-field-error>Please enter a valid time.</p>
</div>`,
})
export class TimeFieldInvalidDemo {}

Controlled

Control the value to synchronize with other components or state management.

Current value: (empty)

import { Component, signal } from '@angular/core';
import { getLocalTimeZone, now, Time, type Time as TimeType } from '@internationalized/date';
import {
  AvButtonComponent,
  AvTimeFieldImports,
  AvDescriptionComponent,
  AvLabelComponent,
} from '@avesra/angular';

@Component({
  selector: 'app-time-field-controlled-demo',
  imports: [
    AvTimeFieldImports,
    AvLabelComponent,
    AvDescriptionComponent,
    AvButtonComponent,
  ],
  template: `<div class="flex flex-col gap-4">
  <div av-time-field class="w-[256px]" name="time" [(value)]="value">
    <label av-label>Time</label>
    <div av-date-input-group>
      <div av-date-input-group-input></div>
    </div>
    <p av-description>Current value: {{ value() ? value()!.toString() : '(empty)' }}</p>
  </div>
  <div class="flex gap-2">
    <button av-button variant="tertiary" type="button" (click)="setNow()">Set now</button>
    <button av-button variant="tertiary" type="button" (click)="value.set(null)">Clear</button>
  </div>
</div>`,
})
export class TimeFieldControlledDemo {
  readonly value = signal<TimeType | null>(null);

  setNow(): void {
    const t = now(getLocalTimeZone());
    this.value.set(new Time(t.hour, t.minute, t.second));
  }
}

Form Example

Complete form example with validation and submission handling.

Enter a time between 9:00 AM and 5:00 PM

import { Component, signal } from '@angular/core';
import { parseTime, type Time as TimeType } from '@internationalized/date';
import {
  AvButtonComponent,
  AvTimeFieldImports,
  AvDescriptionComponent,
  AvFieldErrorComponent,
  AvFormComponent,
  AvLabelComponent,
} from '@avesra/angular';
import { AppIconComponent } from '../../components/app-icon/app-icon.component';

@Component({
  selector: 'app-time-field-form-example-demo',
  imports: [
    AvFormComponent,
    AvTimeFieldImports,
    AvLabelComponent,
    AvDescriptionComponent,
    AvFieldErrorComponent,
    AvButtonComponent,
    AppIconComponent,
  ],
  template: `<form av-form class="flex w-[280px] flex-col gap-4" (submit)="onSubmit($event)">
  <div
    av-time-field
    class="w-full"
    name="time"
    full-width
    required
    [invalid]="isInvalid()"
    [min-value]="minTime"
    [max-value]="maxTime"
    [(value)]="value"
  >
    <label av-label required>Appointment time</label>
    <div av-date-input-group full-width>
      <div av-date-input-group-prefix>
        <app-icon icon="solar:clock-circle-linear" size="16" class="text-muted" />
      </div>
      <div av-date-input-group-input></div>
    </div>
    @if (isInvalid()) {
      <p av-field-error>Time must be between 9:00 AM and 5:00 PM</p>
    } @else {
      <p av-description>Enter a time between 9:00 AM and 5:00 PM</p>
    }
  </div>
  <button
    av-button
    class="w-full"
    type="submit"
    variant="primary"
    [disabled]="!value() || isInvalid()"
    [pending]="isSubmitting()"
  >
    {{ isSubmitting() ? 'Submitting...' : 'Submit' }}
  </button>
</form>`,
})
export class TimeFieldFormExampleDemo {
  readonly minTime = parseTime('09:00');
  readonly maxTime = parseTime('17:00');
  readonly value = signal<TimeType | null>(null);
  readonly isSubmitting = signal(false);

  isInvalid(): boolean {
    const v = this.value();
    return v !== null && (v.compare(this.minTime) < 0 || v.compare(this.maxTime) > 0);
  }

  onSubmit(event: Event): void {
    event.preventDefault();
    if (!this.value() || this.isInvalid()) {
      return;
    }
    this.isSubmitting.set(true);
    setTimeout(() => {
      this.value.set(null);
      this.isSubmitting.set(false);
    }, 1500);
  }
}

With Validation

TimeField supports validation with min-value, max-value, and custom invalid state.

Enter a time between 9:00 AM and 5:00 PM

import { Component, signal } from '@angular/core';
import { parseTime, type Time as TimeType } from '@internationalized/date';
import {
  AvTimeFieldImports,
  AvDescriptionComponent,
  AvFieldErrorComponent,
  AvLabelComponent,
} from '@avesra/angular';

@Component({
  selector: 'app-time-field-with-validation-demo',
  imports: [
    AvTimeFieldImports,
    AvLabelComponent,
    AvDescriptionComponent,
    AvFieldErrorComponent,
  ],
  template: `<div class="flex flex-col gap-4">
  <div
    av-time-field
    class="w-[256px]"
    name="time"
    required
    [invalid]="isInvalid()"
    [min-value]="minTime"
    [max-value]="maxTime"
    [(value)]="value"
  >
    <label av-label required>Time</label>
    <div av-date-input-group>
      <div av-date-input-group-input></div>
    </div>
    @if (isInvalid()) {
      <p av-field-error>Time must be between 9:00 AM and 5:00 PM</p>
    } @else {
      <p av-description>Enter a time between 9:00 AM and 5:00 PM</p>
    }
  </div>
</div>`,
})
export class TimeFieldWithValidationDemo {
  readonly minTime = parseTime('09:00');
  readonly maxTime = parseTime('17:00');
  readonly value = signal<TimeType | null>(null);

  isInvalid(): boolean {
    const v = this.value();
    return v !== null && (v.compare(this.minTime) < 0 || v.compare(this.maxTime) > 0);
  }
}

Customization

Tailwind CSS

import { Component } from '@angular/core';
import {
  AvTimeFieldImports,
  AvLabelComponent,
} from '@avesra/angular';

@Component({
  selector: 'app-time-field-custom-styles-demo',
  imports: [
    AvTimeFieldImports,
    AvLabelComponent,
  ],
  template: `<div av-time-field class="w-[256px]" name="meeting-time">
  <label av-label>Meeting time</label>
  <div
    av-date-input-group
    variant="secondary"
    class="rounded-xl border border-border/80 bg-default shadow-sm"
  >
    <div av-date-input-group-input></div>
  </div>
</div>`,
})
export class TimeFieldCustomStylesDemo {}

Global CSS

To customize the TimeField component classes, you can use the @layer components directive. Learn more .

@layer components {
  .av-time-field {
    @apply flex flex-col gap-1;

    &[data-invalid="true"],
    &[aria-invalid="true"] {
      [data-slot="description"] {
        @apply hidden;
      }
    }

    [data-slot="label"] {
      @apply w-fit;
    }

    [data-slot="description"] {
      @apply px-1;
    }
  }
}

Styling Reference

Avesra follows the BEM methodology so variants and states are reusable and easy to customize.

CSS Classes

  • .av-time-field — Root container
  • .av-time-field--full-width — Full width modifier
  • .av-date-input-group — Input group shell (shared with DateField)
  • .av-date-input-group--primary / --secondary — Variants
  • .av-date-input-group__input — Segment row
  • .av-date-input-group__segment — Editable / literal segment
  • .av-date-input-group__prefix / __suffix — Affixes

Interactive States

  • [data-invalid="true"] — Hides description when invalid
  • [data-required="true"] — Required field
  • [data-disabled="true"] — Disabled
  • [data-focus-within="true"] — Focus inside segments

API Reference

PropTypeDefaultDescription
value / default-valueTime | CalendarDateTime | ZonedDateTime | nullnullSelected time value from @internationalized/date (TimeValue). Implements ControlValueAccessor.
granularity'hour' | 'minute' | 'second''minute'Smallest unit displayed in the segmented input.
hour-cycle12 | 24undefinedForce 12- or 24-hour clock display.
localestringLOCALE_IDBCP 47 locale for segment order and formatting.
placeholder-valueTime | CalendarDateTime | ZonedDateTime | nullnullInfluences placeholder formatting when value is empty.
min-value / max-valueTime | CalendarDateTime | ZonedDateTime | nullnullInclusive time-of-day bounds for committed values.
disabled / readonly / invalid / requiredbooleanfalseInteraction and validation states mirrored to data-* attributes.
full-widthbooleanfalseExpands the field to the full width of its container.
name / idstringundefinedHidden time form field name and root element id.
DateInputGroup variant'primary' | 'secondary''primary'Visual variant for av-date-input-group (shared with DateField).

Made with ❤ by SyntaxHertz. Open source and available on GitHub.