Source Code

<div class="td-card">
  <label class="td-lbl">Duration</label>
  <div class="td-input" id="td">
    <div class="td-seg" data-unit="h" data-max="99">
      <button type="button" class="td-up" aria-label="Increase hours">+</button>
      <input type="text" inputmode="numeric" value="01" aria-label="Hours">
      <button type="button" class="td-down" aria-label="Decrease hours">\u2212</button>
      <span class="td-cap">hr</span>
    </div>
    <span class="td-colon">:</span>
    <div class="td-seg" data-unit="m" data-max="59">
      <button type="button" class="td-up" aria-label="Increase minutes">+</button>
      <input type="text" inputmode="numeric" value="30" aria-label="Minutes">
      <button type="button" class="td-down" aria-label="Decrease minutes">\u2212</button>
      <span class="td-cap">min</span>
    </div>
    <span class="td-colon">:</span>
    <div class="td-seg" data-unit="s" data-max="59">
      <button type="button" class="td-up" aria-label="Increase seconds">+</button>
      <input type="text" inputmode="numeric" value="00" aria-label="Seconds">
      <button type="button" class="td-down" aria-label="Decrease seconds">\u2212</button>
      <span class="td-cap">sec</span>
    </div>
  </div>
  <div class="td-out" id="tdOut"></div>
</div>

Time Duration Input — Free HH MM SS Picker JS Snippet

Time Duration Input · Forms · Plain HTML, CSS & JS · Live preview

What's included

Features

Total-seconds model
One value drives display, rollover, and clamping.
Automatic rollover
59 seconds + 1 carries into minutes with no carry code.
Steppers and arrows
Buttons and arrow keys share one increment path.
Digit sanitizing
Non-numerics stripped, capped at two characters live.
Clamp and pad on blur
Segments settle to valid, zero-padded values.
Dual output
Total seconds plus a formatted HH:MM:SS string.
Symmetric hydrate
setFromTotal() loads an existing duration to edit.
No dependency
Pure HTML/CSS/JS — no date or picker library.

About this UI Snippet

Time Duration Input — HH:MM:SS Picker with Rollover

Screenshot of the Time Duration Input snippet rendered live

A duration input lets a user enter a length of time — for a timer, a video clip, a cooking step, or a booking — as separate hours, minutes, and seconds fields rather than one confusing number. Unlike a clock time picker, it represents an elapsed span and rolls over correctly: bumping seconds past 59 should add a minute. This snippet builds that in plain HTML, CSS, and vanilla JavaScript with no dependency.

One source of truth: total seconds

The trick that makes rollover and clamping trivial is to treat the whole control as a single total-seconds value and derive the display from it. Stepping any segment converts the current fields to a total, adds the segment's weight (3600, 60, or 1), and calls setFromTotal(), which clamps to a max and re-distributes with Math.floor(total/3600), (total%3600)/60, and total%60. So pressing "+" on seconds at 59 naturally carries into minutes — no special-case carry logic anywhere.

Per-segment steppers and arrow keys

Each segment has up and down buttons and listens for ArrowUp/ArrowDown while focused, all routed through the same total-based stepping. That gives mouse and keyboard users the same increment behaviour, and because everything funnels through setFromTotal(), the segments always stay consistent (you can't end up at 01:60:00).

Forgiving typed input

Typing is sanitized live — non-digits are stripped and the value is capped at two characters — but not aggressively reformatted mid-keystroke. On blur each field is clamped to its max (59 for minutes and seconds) and zero-padded, so users can type freely and always land on a valid, padded value like 05.

Live machine-readable output

The readout shows the total in seconds (with toLocaleString() grouping) alongside the padded HH:MM:SS string — the two forms you actually submit or store. Seconds is the canonical value for APIs and <input type="hidden">, while the formatted string is for display.

Reusing the value

Bind totalSeconds() to your form state and you have a clean integer to persist, compare, or feed into a countdown. The same total can hydrate the control on load via setFromTotal(), so editing an existing duration is symmetric with creating one.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Ask an AI coding assistant like Claude to explain why every stepper button and arrow-key handler routes through totalSeconds() and setFromTotal() instead of incrementing each segment's own value directly — that single-source-of-truth design is exactly what makes 59 seconds plus one automatically roll into a new minute with zero explicit carry logic. It's also worth asking whether clamping to 99 hours in setFromTotal() is the right ceiling for your use case, or whether it should be configurable. For extending it, ask for a version that also accepts pasted "1h 30m" style text and parses it back into the total-seconds model, a compact display mode that only shows non-zero units, or a way to bind two of these side by side and validate that an end duration is longer than a start duration. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a duration input for hours, minutes, and seconds in plain HTML, CSS, and JavaScript that models the value as a single total-seconds number, not three independent fields.

Requirements:
- Three segments (hours, minutes, seconds), each with its own text input, an increment button, and a decrement button, plus a live readout showing both the raw total in seconds and a formatted HH:MM:SS string.
- Implement one function that reads all three segment inputs and combines them into a single total-seconds integer, and one inverse function that takes a total-seconds integer, clamps it between zero and a maximum (such as 99 hours 59 minutes 59 seconds), and redistributes it back into the three zero-padded segment values using division and modulo by 3600 and 60 — this redistribution function must be the only place that writes segment values.
- Every increment and decrement button, and every ArrowUp/ArrowDown keydown on a focused segment, must work by computing the current total, adding or subtracting that segment's unit weight (3600, 60, or 1 second), and passing the result through the single redistribution function — never mutating a segment's displayed value directly.
- While typing, strip non-digit characters live and cap each segment's input length, but only clamp the value to its maximum and re-pad it with a leading zero when the field loses focus, not on every keystroke.
- Confirm that incrementing seconds at 59 correctly carries into minutes, and minutes at 59 correctly carries into hours, purely as a side effect of the redistribution function's floor and modulo math, with no special-cased carry branches anywhere in the code.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSHours, minutes, and seconds segments render with steppers and a total.
  2. 2
    Use the + and - buttonsEach segment steps by its unit and rolls over into the next.
  3. 3
    Type a valueOnly digits are accepted, capped at two per segment.
  4. 4
    Press arrow keysArrowUp and ArrowDown step the focused segment.
  5. 5
    Read the outputThe total seconds and a padded HH:MM:SS string update live.
  6. 6
    Bind the valueUse totalSeconds() as the integer you store or submit.

Real-world uses

Common Use Cases

Timers and alarms
Set a length that feeds a circular countdown.
Video and media editors
Enter a clip duration next to a video player.
Booking and scheduling
Pick a slot length beside a time slot picker.
Workout and cooking apps
Set step durations alongside a pomodoro timer.
Stepper-style inputs
Pair with a quantity stepper for numeric fields.
Learning input modeling
A reference for single-source-of-truth field groups.

Got questions?

Frequently Asked Questions

The control treats everything as a single total-seconds number. Stepping a segment converts the fields to a total, adds the unit weight (3600, 60, or 1), then setFromTotal() re-derives hours, minutes, and seconds with floor and modulo. So incrementing seconds at 59 automatically adds a minute — there's no manual carry logic, which is where hand-rolled time inputs usually have bugs.

A time picker selects a clock time (like 14:30, a point in the day), while a duration input represents an elapsed length (like 1 hour 30 minutes). Durations need rollover and an unbounded-ish hours field, and the natural output is total seconds rather than a timestamp, which is what this snippet models.

Reformatting on every keystroke fights the caret and makes typing awkward. Instead, input is sanitized to digits live, and on blur each segment is clamped to its max (59 for minutes and seconds) and zero-padded. Users type freely and always end on a valid, consistently padded value.

Store the integer from totalSeconds(). It's compact, easy to compare and sum, and unambiguous across time zones and formats. Render the HH:MM:SS string only for display, and hydrate the control from a stored total with setFromTotal() when editing an existing duration.

Keep a single totalSeconds value in state and derive the three segment displays from it in render. Step handlers update that one number; setFromTotal becomes setState(clamp(newTotal)). Sanitize typed input in the change handler and clamp on blur. The integer maps directly to your form model, and Tailwind handles the segment layout with flex utilities.