# Time

Time display components for showing current time, duration, and remaining time in a video player

## Import

```tsx
import { Time } from '@videojs/react';
```

## Anatomy

```tsx
<Time.Group>
  <Time.Value type="current" />
  <Time.Separator />
  <Time.Value type="duration" />
</Time.Group>
```

## Behavior

Three display types — `current`, `duration`, and `remaining` — in digital format with smart padding:

- **Hours** are never padded (`1:05:30`, not `01:05:30`)
- **Minutes** are padded when hours are shown (`1:05:30`, but `5:30`)
- **Seconds** are always padded (`1:05`, not `1:5`)

Hour display is triggered when either the current value or the duration exceeds 1 hour, ensuring consistency within a Group. Remaining time displays a negative sign (customizable via the `negativeSign` prop).

Use `toggle` to let current displays switch between elapsed and remaining time, or remaining and duration displays switch between those two values. The initial display comes from `type`.

Before the media reports a duration or seekable range — while metadata is still loading, for example — no time value is available. A plain display still renders the formatted zero value but sets `data-unavailable`; a toggleable display is disabled instead: it sets `data-disabled`, leaves the tab order, and ignores activation until a value arrives.

```tsx
<Time.Value toggle />
<Time.Value toggle type="remaining" />
<Time.Value toggle type="duration" />
```

## Styling

| Attribute | Values | Description |
| --- | --- | --- |
| `data-type` | `current` / `duration` / `remaining` | The type of time being displayed |
| `data-disabled` | Present / absent | Present when a toggleable display has no time value yet |
| `data-unavailable` | Present / absent | Present when a plain display has no time value yet |

The negative sign is rendered inside `<span aria-hidden="true">` and can be hidden with CSS:

```css
[data-type="remaining"] > span[aria-hidden] {
  display: none;
}
```

Dim a display while its value is unavailable:

```css
[data-disabled],
[data-unavailable] {
  opacity: 0.5;
}
```

## Accessibility

Each `<Time.Value>` has:

- a native `<time datetime>` element for machine-readable time semantics
- `aria-label` for the static role label (“Current time”, “Duration”, “Remaining”)

No `aria-live` region is used — time updates too frequently and might overwhelm screen readers. The separator and negative sign are `aria-hidden="true"` because the accessible label already describes the value.

Toggleable time displays receive `role="button"`, `tabindex="0"`, and keyboard support for Enter and Space. Their `aria-label` starts with the action and includes the current value. Their `aria-description` explains which values the control toggles between.

While no time value is available, a toggleable display is disabled: it sets `aria-disabled="true"` and `tabindex="-1"`, drops the `aria-description`, changes its `aria-label` to “Media not loaded, unknown time.”, and ignores clicks and key presses. A plain display in the same state omits `datetime` and uses that same label.

## Examples

### Current Time

**App.tsx**

```tsx
import { Container, createPlayer, Time } from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';

const { Player } = createPlayer({ features: videoFeatures });

export default function CurrentTime() {
  return (
    <Player>
      <Container className="media-container">
        <Video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" autoPlay muted playsInline loop />
        <Time.Value type="current" className="media-time" />
      </Container>
    </Player>
  );
}
```

**App.css**

```css
.media-container {
  position: relative;
}

.media-container video {
  width: 100%;
}

.media-time {
  position: absolute;
  bottom: 10px;
  left: 10px;
  padding-block: 8px;
  padding-inline: 20px;
  font-variant-numeric: tabular-nums;
  color: black;
  background: rgba(255, 255, 255, 0.7);
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 9999px;
  backdrop-filter: blur(10px);
}
```

### Current / Duration

**App.tsx**

```tsx
import { Container, createPlayer, Time } from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';

const { Player } = createPlayer({ features: videoFeatures });

export default function CurrentDuration() {
  return (
    <Player>
      <Container className="media-container">
        <Video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" autoPlay muted playsInline loop />
        <Time.Group className="time-group">
          <Time.Value type="current" />
          <Time.Separator />
          <Time.Value type="duration" />
        </Time.Group>
      </Container>
    </Player>
  );
}
```

**App.css**

```css
.media-container {
  position: relative;
}

.media-container video {
  width: 100%;
}

.time-group {
  position: absolute;
  bottom: 10px;
  left: 10px;
  display: flex;
  gap: 4px;
  align-items: center;
  padding-block: 8px;
  padding-inline: 20px;
  font-variant-numeric: tabular-nums;
  color: black;
  background: rgba(255, 255, 255, 0.7);
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 9999px;
  backdrop-filter: blur(10px);
}
```

### Remaining

**App.tsx**

```tsx
import { Container, createPlayer, Time } from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';

const { Player } = createPlayer({ features: videoFeatures });

export default function Remaining() {
  return (
    <Player>
      <Container className="media-container">
        <Video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" autoPlay muted playsInline loop />
        <Time.Value type="remaining" className="media-time" />
      </Container>
    </Player>
  );
}
```

**App.css**

```css
.media-container {
  position: relative;
}

.media-container video {
  width: 100%;
}

.media-time {
  position: absolute;
  bottom: 10px;
  left: 10px;
  padding-block: 8px;
  padding-inline: 20px;
  font-variant-numeric: tabular-nums;
  color: black;
  background: rgba(255, 255, 255, 0.7);
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 9999px;
  backdrop-filter: blur(10px);
}
```

### Custom Separator

**App.tsx**

```tsx
import { Container, createPlayer, Time } from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';

const { Player } = createPlayer({ features: videoFeatures });

export default function CustomSeparator() {
  return (
    <Player>
      <Container className="media-container">
        <Video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" autoPlay muted playsInline loop />
        <Time.Group className="time-group">
          <Time.Value type="current" />
          <Time.Separator> of </Time.Separator>
          <Time.Value type="duration" />
        </Time.Group>
      </Container>
    </Player>
  );
}
```

**App.css**

```css
.media-container {
  position: relative;
}

.media-container video {
  width: 100%;
}

.time-group {
  position: absolute;
  bottom: 10px;
  left: 10px;
  display: flex;
  gap: 4px;
  align-items: center;
  padding-block: 8px;
  padding-inline: 20px;
  font-variant-numeric: tabular-nums;
  color: black;
  background: rgba(255, 255, 255, 0.7);
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 9999px;
  backdrop-filter: blur(10px);
}
```

### Custom Negative Sign

**App.tsx**

```tsx
import { Container, createPlayer, Time } from '@videojs/react';
import { Video, videoFeatures } from '@videojs/react/video';

const { Player } = createPlayer({ features: videoFeatures });

export default function CustomNegativeSign() {
  return (
    <Player>
      <Container className="media-container">
        <Video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/highest.mp4" autoPlay muted playsInline loop />
        <Time.Value type="remaining" negativeSign="~" className="media-time" />
      </Container>
    </Player>
  );
}
```

**App.css**

```css
.media-container {
  position: relative;
}

.media-container video {
  width: 100%;
}

.media-time {
  position: absolute;
  bottom: 10px;
  left: 10px;
  padding-block: 8px;
  padding-inline: 20px;
  font-variant-numeric: tabular-nums;
  color: black;
  background: rgba(255, 255, 255, 0.7);
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 9999px;
  backdrop-filter: blur(10px);
}
```

## API Reference

### Value

Displays a formatted time value (current, duration, or remaining).

#### Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | `{ key: string; text: string } \| string \| ((state: TimeState) => Text \| string)` | `''` | Custom label for accessibility. |
| `negativeSign` | `string` | `'-'` | Symbol prepended to remaining time. |
| `toggle` | `boolean` | `false` | Whether the time display can be toggled. |
| `type` | `'current' \| 'duration' \| 'remaining'` | `'current'` | Which time value to display. |

#### State

State is accessible via the `render`, `className`, and `style` props.

| Property | Type | Description |
| --- | --- | --- |
| `type` | `'current' \| 'duration' \| 'remaining'` | Time display type. |
| `disabled` | `boolean` | Whether the time toggle is disabled. |
| `unavailable` | `boolean` | Whether the non-interactive time value is unavailable. |
| `seconds` | `number` | Raw value in seconds. |
| `negative` | `boolean` | Whether the time value is negative (remaining time before end). |
| `text` | `string` | Formatted display text without sign (e.g., "1:30"). |
| `phrase` | `string` | Human-readable phrase (e.g., "1 minute, 30 seconds"). |
| `datetime` | `string` | ISO 8601 duration (e.g., "PT1M30S"). |

#### Data attributes

| Attribute | Type | Description |
| --- | --- | --- |
| `data-type` | `'current' \| 'duration' \| 'remaining'` | The type of time being displayed. |
| `data-disabled` | — | Present when the time toggle is disabled. |
| `data-unavailable` | — | Present when the non-interactive time value is unavailable. |

### Group

Container for composed time displays. Renders a `<span>` element.

#### Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | — | Time value components to render inside the group. |
| `className` | `string \| ((state: Record<string, never>) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: Record<string, never>) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: Record<string, never>) => CSSProperties \| undefined)` | — | Style or function returning style from state. |

### Separator

Divider between time values. Hidden from screen readers.

#### Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | — | Separator content. Defaults to "/". |
| `className` | `string \| ((state: Record<string, never>) => string \| undefined)` | — | Class name or function returning class name from state. |
| `render` | `ReactElement \| ((props: HTMLProps, state: Record<string, never>) => ReactElement \| null)` | — | Render prop for custom element. |
| `style` | `CSSProperties \| ((state: Record<string, never>) => CSSProperties \| undefined)` | — | Style or function returning style from state. |

---

React documentation: https://videojs.org/docs/framework/react/llms.txt
All documentation: https://videojs.org/llms.txt
