Skip to content

Early previewhamur is not on npm yet. The components are being built in the open, follow along on GitHub.

DisplayReact

Timer

A countdown or a stopwatch read from the wall clock.

Rendered by React 19.3.1

Loading React…
import { Timer } from '@hamur/react/timer'

export default function TimerExample() {
  return (
    <Timer.Root className="timer" duration={1500000}>
      <Timer.Label className="timer-label">
        Focus session
      </Timer.Label>
      <Timer.Value className="timer-value" />
      <div className="timer-bar" />
      <div className="timer-controls">
        <Timer.Trigger
          className="button button-square timer-toggle"
          action="toggle"
        >
          <svg className="timer-icon-play" width="16" height="16" viewBox="0 0 24 24">
            <polygon points="6 3 20 12 6 21 6 3" />
          </svg>
          <svg className="timer-icon-pause" width="16" height="16" viewBox="0 0 24 24">
            <rect width="4" height="16" x="14" y="4" rx="1" />
            <rect width="4" height="16" x="6" y="4" rx="1" />
          </svg>
        </Timer.Trigger>
        <Timer.Trigger className="button button-square timer-reset" action="reset">
          <svg width="16" height="16" viewBox="0 0 24 24">
            <path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
            <path d="M3 3v5h5" />
          </svg>
        </Timer.Trigger>
      </div>
    </Timer.Root>
  )
}
Not measured yetTests passing in React
Not measured yetSize
Not measured yetMemory
Not measured yetSpeed
[N] commits

Installation

npm i @hamur/react

Not on npm yet. The package goes out under the chosen name.

Anatomy

Import the component and assemble its parts:

<Timer.Root>
  <Timer.Label />
  <Timer.Value />
  <Timer.Trigger />
</Timer.Root>

// Not in this example: Timer.Segment, Timer.SegmentLabel, Timer.Separator

Examples

Stopwatch

A stopwatch drawn in Segments to the hundredth; a Lap button reads the time from actionsRef, Reset clears it.

TimerStopwatch.tsx
import * as React from 'react'
import { Timer } from '@hamur/react/timer'

export default function TimerStopwatchExample() {
  const actions = React.useRef<Timer.Root.Actions | null>(null)
  const [laps, setLaps] = React.useState<number[]>([])
  const lap = () => {
    const time = actions.current?.getTime()
    if (time) setLaps((previous) => [...previous, time.total])
  }
  return (
    <div className="stack">
      <Timer.Root
        className="timer"
        mode="stopwatch"
        interval={10}
        largestUnit="minutes"
        actionsRef={actions}
        onValueChange={(value) => {
          if (value.elapsed === 0 && value.startedAt === null) setLaps([])
        }}
      >
        <Timer.Label className="timer-label">Stopwatch</Timer.Label>
        <Timer.Value className="timer-value">
          <Timer.Segment unit="minutes" />
          <Timer.Separator />
          <Timer.Segment unit="seconds" />
          <Timer.Separator>.</Timer.Separator>
          <Timer.Segment className="timer-fraction" unit="milliseconds" />
        </Timer.Value>
        <div className="timer-controls">
          <Timer.Trigger className="button button-square timer-toggle" action="toggle">
            <svg className="timer-icon-play" width="16" height="16" viewBox="0 0 24 24">
              <polygon points="6 3 20 12 6 21 6 3" />
            </svg>
            <svg className="timer-icon-pause" width="16" height="16" viewBox="0 0 24 24">
              <rect width="4" height="16" x="14" y="4" rx="1" />
              <rect width="4" height="16" x="6" y="4" rx="1" />
            </svg>
          </Timer.Trigger>
          <button type="button" className="button" onClick={lap}>
            Lap
          </button>
          <Timer.Trigger className="button" action="reset">
            Reset
          </Timer.Trigger>
        </div>
      </Timer.Root>
      <ol className="timer-laps">
        {laps.map((total, index) => (
          <li key={index}>
            Lap {index + 1} · {(total / 1000).toFixed(2)} s
          </li>
        ))}
      </ol>
    </div>
  )
}

Target

A countdown to midnight UTC in hours, minutes and seconds with their SegmentLabels, and a toggle that pauses it.

TimerTarget.tsx
import * as React from 'react'
import { Timer } from '@hamur/react/timer'

const nextMidnight = () => {
  const date = new Date()
  date.setUTCHours(24, 0, 0, 0)
  return date.getTime()
}

export default function TimerTargetExample() {
  const [target] = React.useState(nextMidnight)
  return (
    <Timer.Root className="timer" target={target} largestUnit="hours">
      <Timer.Label className="timer-label">The sale ends at midnight UTC, in</Timer.Label>
      <Timer.Value className="timer-value timer-units">
        <Timer.Segment unit="hours" />
        <Timer.SegmentLabel className="timer-unit" unit="hours" unitDisplay="short" />
        <Timer.Segment unit="minutes" />
        <Timer.SegmentLabel className="timer-unit" unit="minutes" unitDisplay="short" />
        <Timer.Segment unit="seconds" />
        <Timer.SegmentLabel className="timer-unit" unit="seconds" unitDisplay="short" />
      </Timer.Value>
      <div className="timer-controls">
        <Timer.Trigger className="button button-square timer-toggle" action="toggle">
          <svg className="timer-icon-play" width="16" height="16" viewBox="0 0 24 24">
            <polygon points="6 3 20 12 6 21 6 3" />
          </svg>
          <svg className="timer-icon-pause" width="16" height="16" viewBox="0 0 24 24">
            <rect width="4" height="16" x="14" y="4" rx="1" />
            <rect width="4" height="16" x="6" y="4" rx="1" />
          </svg>
        </Timer.Trigger>
      </div>
    </Timer.Root>
  )
}

Extend

A sign-out countdown that starts on mount, says "1 minute left" and can be extended by a minute.

TimerExtend.tsx
import * as React from 'react'
import { Timer } from '@hamur/react/timer'

export default function TimerExtendExample() {
  const [log, setLog] = React.useState('none')
  return (
    <div className="stack">
      <Timer.Root
        className="timer"
        duration={120000}
        autoStart
        announce={[60000]}
        labels={{ completed: 'You are signed out' }}
        onValueChange={(_, details) => setLog(details.reason)}
        onComplete={() => setLog('signed out')}
      >
        <Timer.Label className="timer-label">You will be signed out in</Timer.Label>
        <Timer.Value className="timer-value" />
        <div className="timer-controls">
          <Timer.Trigger className="button" action="add">
            Add 1 minute
          </Timer.Trigger>
        </div>
      </Timer.Root>
      <p className="muted timer-log">last change {log}</p>
    </div>
  )
}

API reference

26 props across 7 parts, in React syntax. className, style, render are listed once, under Shared props.

LabelTimer.Label

Names the Value: what the time measures. Renders a <span>.

Label data attributes
Data attributeDescription
data-idlePresent while the timer has not started.
data-runningPresent while the timer runs.
data-pausedPresent while the timer is paused.
data-completedPresent once the time has reached its end.
data-overtimePresent while the time is past its end with overtime.
data-disabledPresent when the component is disabled.
data-pendingPresent while a target countdown waits for mount to draw its time.

RootTimer.Root

Groups all parts of the timer: a countdown or a stopwatch with its controls. Renders a <div> element, with the status region as its last child. Renders a <div>.

Root props
PropTypeDefault
modeWhether the timer shows the time left or the time elapsed.TimerMode'countdown'
durationA countdown's length, or a stopwatch's limit, in milliseconds.number—
targetAn instant to count down to on the wall clock; runs from mount.TimerTarget—
valueThe run: the milliseconds run before startedAt, and the wall-clock start of the currentTimerValue—
defaultValueThe run when the timer is initially rendered.TimerValue{ elapsed: 0, startedAt: null }
onValueChangeEvent handler called when an action or the completion changes the run; never per tick.((value: TimerValue, eventDetails: TimerRootValueChangeEventDetails) => void)—
autoStartWhether the timer starts once after mount.booleanfalse
intervalThe step of the shown time, in milliseconds.number1000
largestUnitThe largest unit the time is balanced into: 'hours' shows 50:00:00 for two days and twoTimerLargestUnit'days'
overtimeWhether the time keeps counting past its end.booleanfalse
announceShown times, in milliseconds, at which the status region says the time.readonly number[][]
localeThe locale of the digits, the separators and the unit words.Intl.LocalesArgument—
labelsThe texts, in English by default: the Triggers' names and the announcements.Partial<TimerLabels>—
nowThe clock, read on every tick and action.(() => number)Date.now
onTickEvent handler called on each change of the shown time while the page is visible.((time: TimerTime) => void)—
onCompleteEvent handler called once per run when the time reaches its end.((eventDetails: { reason: "complete"; event: MouseEvent | Event | PointerEvent | KeyboardEvent; } & { overdue: number; }) => void)—
disabledWhether the component should ignore user interaction.booleanfalse
actionsRefA ref to imperative actions: start(), pause(), resume(), toggle(), reset(),React.RefObject<TimerRootActions | null>—
Root data attributes
Data attributeDescription
data-idlePresent while the timer has not started.
data-runningPresent while the timer runs.
data-pausedPresent while the timer is paused.
data-completedPresent once the time has reached its end.
data-overtimePresent while the time is past its end with overtime.
data-disabledPresent when the component is disabled.
data-pendingPresent while a target countdown waits for mount to draw its time.
Root CSS variables
CSS variableDescription
--timer-progressThe fraction of duration elapsed, from 0 to 1. Not set without duration.

SegmentTimer.Segment

One unit of the time, balanced from largestUnit and padded to two digits. Place it inside the Value. Renders a <span>.

Segment props
PropTypeDefault
unitrequiredThe unit the segment shows.TimerUnit—
Segment data attributes
Data attributeDescription
data-idlePresent while the timer has not started.
data-runningPresent while the timer runs.
data-pausedPresent while the timer is paused.
data-completedPresent once the time has reached its end.
data-overtimePresent while the time is past its end with overtime.
data-disabledPresent when the component is disabled.
data-pendingPresent while a target countdown waits for mount to draw its time.
data-unitThe unit the part shows.

SegmentLabelTimer.SegmentLabel

The word of a unit for its value in the locale: "minute" or "minutes". Its children replace it. Renders a <span>.

SegmentLabel props
PropTypeDefault
unitrequiredThe unit the label names.TimerUnit—
unitDisplayThe length of the word: "minutes", "min" or "m".TimerUnitDisplay'long'
SegmentLabel data attributes
Data attributeDescription
data-idlePresent while the timer has not started.
data-runningPresent while the timer runs.
data-pausedPresent while the timer is paused.
data-completedPresent once the time has reached its end.
data-overtimePresent while the time is past its end with overtime.
data-disabledPresent when the component is disabled.
data-pendingPresent while a target countdown waits for mount to draw its time.
data-unitThe unit the part shows.

SeparatorTimer.Separator

The locale's time separator between Segments: ":" in English. Its children replace it. Renders a <span>.

Separator data attributes
Data attributeDescription
data-idlePresent while the timer has not started.
data-runningPresent while the timer runs.
data-pausedPresent while the timer is paused.
data-completedPresent once the time has reached its end.
data-overtimePresent while the time is past its end with overtime.
data-disabledPresent when the component is disabled.
data-pendingPresent while a target countdown waits for mount to draw its time.

TriggerTimer.Trigger

A button that acts on the run: toggle starts, pauses, resumes or restarts by status, and its name follows. While its action does not apply it is aria-disabled and keeps focus. Renders a <button>.

Trigger props
PropTypeDefault
nativeButtonWhether the component renders a native <button> element when replacing itbooleantrue
actionrequiredWhat the button does: toggle starts, pauses, resumes or restarts by status.TimerAction—
amountThe milliseconds an add button adds.number60000
focusableWhenDisabledWhether the button stays focusable when disabled.booleantrue
Trigger data attributes
Data attributeDescription
data-idlePresent while the timer has not started.
data-runningPresent while the timer runs.
data-pausedPresent while the timer is paused.
data-completedPresent once the time has reached its end.
data-overtimePresent while the time is past its end with overtime.
data-disabledPresent when the component is disabled.
data-pendingPresent while a target countdown waits for mount to draw its time.
data-actionThe action the button performs.

ValueTimer.Value

The time: a hidden long form for screen readers and the drawn text, or the Segments in its children. Its name is the Label. Renders a <time>.

Value props
PropTypeDefault
childrenReplaces the drawn text, never the spoken one: Segments, or a function of the drawn text andReact.ReactNode | ((formatted: string, time: TimerTime) => React.ReactNode)—
Value data attributes
Data attributeDescription
data-idlePresent while the timer has not started.
data-runningPresent while the timer runs.
data-pausedPresent while the timer is paused.
data-completedPresent once the time has reached its end.
data-overtimePresent while the time is past its end with overtime.
data-disabledPresent when the component is disabled.
data-pendingPresent while a target countdown waits for mount to draw its time.

Shared props

Every part that renders an element takes these.

Shared props
PropType
classNameCSS class applied to the element, or a function that returns a class based on the component's state.string | ((state: State) => string | undefined)
styleStyle applied to the element, or a function that returns a style object based on the component's state.React.CSSProperties | ((state: State) => React.CSSProperties | undefined)
renderAllows you to replace the component's HTML element with a different tag, or compose it with another component.React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, State>

Keyboard and accessibility

Keyboard

Timer has no keys of its own. Each Trigger is a native button, so Enter and Space press it, and Tab moves through them in the order placed. A Trigger whose action does not apply (Reset before a start, Add after the end) stays in the Tab order and ignores the press.

Accessibility

What the parts set
PartSetsSo that
Valuea time with role="timer", aria-live="off", aria-atomic="true" and datetime (PT4M59S), named by the Label through aria-labelledbythe time is read on demand, under a name that never changes, and is never spoken each tick
Valuea visually hidden span with the long form, and the drawn text or the Segments in an aria-hidden="true" span"4 minutes, 59 seconds" is read instead of an ambiguous "4:59"
Labela span whose id names the Valuethe Value says what the time measures
Triggera button of type button named by its text, or "Start", "Pause", "Resume", "Restart", "Reset" or "Add 1 minute" (labels); never aria-pressedthe name says what the press will do, and its change is the feedback for the press
Triggeraria-disabled="true" while its action does not apply or the Root is disabled; it stays focusablefocus stays on the button when it stops applying under it, such as Pause at the end
Roota div with no role, and a visually hidden polite, atomic status region as its last child, empty until a messagethe completion and the milestones are announced once, without moving focus (WCAG 4.1.3)
Segmenta span with the unit's value, inside the hidden spanthe digits are drawn one unit at a time and read once, in the long form
SegmentLabela span with the unit's word in the locale ("minutes", "min"), inside the hidden spanthe drawn units can be labelled without being read twice
Separatora span with the locale's time separator, inside the hidden spanthe separator is drawn but never read

Focus order

  1. Each Trigger is one Tab stop, in the order placed; the Value and the Label take no focus.
  2. Focus stays on a Trigger through the press, the change of its name and the end of the run.
  3. Nothing takes focus on mount, at a milestone or at the end.