Recently while working on a project, I found that the standard HTML Time Input field (see below) wasn’t sufficient for my needs.
<input type="time" id="setTime" name="setTime" />
I needed users to be able to input 24hr time right down to the milliseconds. Something like this:
Features
Mobile Input will display the number pad
Under the hood, each input field is a number. This means that mobile devices should default to the number pad for convenience.
Value retrieval
Values are stored under the name time-<<timeType>>
, for example time-hours
.
You can loop over your form data and retrieve the names as needed or set a custom ID to the field as you create it dynamically with JavaScript. You can see an example of this in the Example 1: Single time field section.
Automatically moves to the next input
Setting field character limitsSetting character limits on each time field allows the cursor to automatically move to the next fields.
Client-Side Validation
Removing ‘Accepted’ number input characters that are not numbers
Yeah, yeah, you know you got to validate server-side too, but it’s good to let the user know that if they accidentally put in an accepted number character like e, +, -
that it will be removed for them automatically.
Cannot exceed the number range
The max and min number ranges are set by default, but you can always change them manually.
Maybe you want to start with minutes, but as a duration and not a time, have minutes that go all the way up to 10,000. All good, just change the max value in the time-minute
number input.
If a user puts in a number greater or less than the default min or max, the number will be replaced by the min or max respectively.
So for example, if the user sets 69 (hur hur hur) minutes but the max minutes is 60 then the field will default to 60.
Likewise, if the user sets the millisecond cell to 2 but your min is 500, then it will default to 500 milliseconds.
This is done with some JavaScript event listeners.
Returning back a tab will clear the field
Returning back a tab by cursor or shift-tab
will clear out the field for the user to start afresh.
Prepends Zeros (0) to Any Number Not At Max Character Length
This is just an aesthetic thing, but it makes a difference. When a number’s character length is less than the maximum character length, then additional zeroes will be added to the front of the number.
Quickly add or remove a time input
The values of the time inputs are formatted using a generated CSS method for number inputs within the timeInput
class. Further event listeners are looped over this class to allow for fewer or greater number-input elements.
As such, you can add days, months and years or, nanoseconds, microseconds and zeptoseconds. Alternatively, you could remove one of the existing time measures from the field.
The Code for the Custom Time Input field
You can grab a basic sample of the code here:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 |
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- For the tutorial: A Custom-Built HTML Time Input Element https://yagisanatode.com/a-custom-build-html-time-input-element/ --> <title>Time</title> </head> <body> <style> .timeInput { display: grid; grid-template-columns: repeat(7, auto); width: 8.5em; } .timeInput input::-webkit-outer-spin-button, .timeInput input::-webkit-inner-spin-button { display: none; margin: 0; } .timeInput .time-lables { font-size: x-small; text-align: center; } .timeInput .time-hours { width: 1.2em; text-align: center; } .timeInput .time-minutes { width: 1.2em; text-align: center; } .timeInput .time-seconds { width: 1.2em; text-align: center; } .timeInput .time-milliseconds { width: 2.7em; text-align: center; } </style> <div class="timeInput"> <span class="time-lables">HH</span> <span class="time-lables"></span> <span class="time-lables">MM</span> <span class="time-lables"></span> <span class="time-lables">SS</span> <span class="time-lables"></span> <span class="time-lables">MS</span> <input class="time-hours" type="number" name="time-hours" min=0 max=24 step=1 placeholder="00" data-maxlen=2> <span>:</span> <input class="time-minutes" type="number" name="time-minutes" min=0 max=60 step=1 placeholder="00" data-maxlen=2> <span>:</span> <input class="time-seconds" type="number" name="time-seconds" min=0 max=60 step=1 placeholder="00" data-maxlen=2> <span>:</span> <input class="time-milliseconds" type="number" name="time-milliseconds" min=0 max=999 step=1 placeholder="000" data-maxlen=4> </div> <br> <br> <br> <input type="time" id="setTime" name="setTime" /> <script type="text/javascript"> /** * Handles the time values for the timeInput element. * - Provides validation * -- Only numbers * -- witin min and max * -- max length * - Moves cursor to next input after max length is exceeded. */ function customTimeHandler() { /** * Validates the input value. * @param {HTMLElement} input */ function validate(input) { let val = input.value // Check for non numbers & remove val = val.replaceAll(/\D/g, "") // If greater than data-maxlen, remove last char val = val.substring(0, input.dataset.maxlen) const valNum = Number(val) // If greater than max, set max if (valNum < input.min) { val = input.min } // If less than min, set min else if (valNum > input.max) { val = input.max } // If less than total number of chars in the placeholder add leading zero const placeholderZeroes = input.placeholder const charLenMax = 10 ** (input.placeholder.length - 1) if (valNum == 0) { val = placeholderZeroes } else if (valNum < charLenMax) { const lenDif = placeholderZeroes.length + 1 - val.length const addedZeros = placeholderZeroes.substring(1, lenDif) val = addedZeros + val } input.value = val } const timeInputElements = document.querySelectorAll(".timeInput") timeInputElements.forEach(timeInputEl => { const timeInputs = timeInputEl.querySelectorAll('input') timeInputs.forEach((input, idx) => { input.addEventListener('focus', function() { input.value = "" }) input.addEventListener('input', function() { const inputLen = input.value.length const maxLen = input.dataset.maxlen if (inputLen >= maxLen) { validate(input) if (idx < timeInputs.length - 1) { timeInputs[idx + 1].focus() } } }) input.addEventListener('focusout', function() { validate(input) }) }) }) } customTimeHandler() </script> </body> </html> |
Example 1: Single time field
In this simple example, we will encapsulate the time input into a form and add a ‘submit’ button. Of course, you can add other form elements to the form as well.
In our script section, we added two functions submitForm()
and getFormData()
that is called on submit and retrieve all values in the form by their ‘name’ attribute.
I’ve put the submit button outside the form so you can see the logged results. If you wish to refresh the page or move to another page you can add the submit button inside the form.
Check out the updated code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 |
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- For the tutorial: A Custom-Built HTML Time Input Element https://yagisanatode.com/a-custom-build-html-time-input-element/ --> <title>Time</title> </head> <body> <style> .timeInput { display: grid; grid-template-columns: repeat(7, auto); width: 8.0em; } .timeInput input::-webkit-outer-spin-button, .timeInput input::-webkit-inner-spin-button { display: none; margin: 0; } .timeInput .time-lables { font-size: x-small; text-align: center; } .timeInput .time-hours { width: 1.2em; text-align: center; } .timeInput .time-minutes { width: 1.2em; text-align: center; } .timeInput .time-seconds { width: 1.2em; text-align: center; } .timeInput .time-milliseconds { width: 2em; text-align: center; } </style> <form id="inputForm"> <div class="timeInput"> <span class="time-lables">HH</span> <span class="time-lables"></span> <span class="time-lables">MM</span> <span class="time-lables"></span> <span class="time-lables">SS</span> <span class="time-lables"></span> <span class="time-lables">MS</span> <input class="time-hours" type="number" name="time-hours" min=0 max=24 step=1 placeholder="00" data-maxlen=2> <span>:</span> <input class="time-minutes" type="number" name="time-minutes" min=0 max=60 step=1 placeholder="00" data-maxlen=2> <span>:</span> <input class="time-seconds" type="number" name="time-seconds" min=0 max=60 step=1 placeholder="00" data-maxlen=2> <span>:</span> <input class="time-milliseconds" type="number" name="time-milliseconds" min=0 max=999 step=1 placeholder="000" data-maxlen=4> </div> <hr> </form> <button onclick="submitForm()">Submit</button> <script type="text/javascript"> /** * Handles the time values for the timeInput element. * - Provides validation * -- Only numbers * -- witin min and max * -- max length * - Moves cursor to next input after max length is exceeded. */ function customTimeHandler() { /** * Validates the input value. * @param {HTMLElement} input */ function validate(input) { let val = input.value // Check for non numbers & remove val = val.replaceAll(/\D/g, "") // If greater than data-maxlen, remove last char val = val.substring(0, input.dataset.maxlen) const valNum = Number(val) // If greater than max, set max if (valNum < input.min) { val = input.min } // If less than min, set min else if (valNum > input.max) { val = input.max } // If less than total number of chars in the placeholder add leading zero const placeholderZeroes = input.placeholder const charLenMax = 10 ** (input.placeholder.length - 1) if (valNum == 0) { val = placeholderZeroes } else if (valNum < charLenMax) { const lenDif = placeholderZeroes.length + 1 - val.length const addedZeros = placeholderZeroes.substring(1, lenDif) val = addedZeros + val } input.value = val } const timeInputElements = document.querySelectorAll(".timeInput") timeInputElements.forEach(timeInputEl => { const timeInputs = timeInputEl.querySelectorAll('input') timeInputs.forEach((input, idx) => { input.addEventListener('focus', function() { input.value = "" }) input.addEventListener('input', function() { const inputLen = input.value.length const maxLen = input.dataset.maxlen if (inputLen >= maxLen) { validate(input) if (idx < timeInputs.length - 1) { timeInputs[idx + 1].focus() } } }) input.addEventListener('focusout', function() { validate(input) }) }) }) } customTimeHandler() /** * ##### SUBMIT FORM ##### * */ // Function to handle form submission function submitForm() { const form = document.getElementById('inputForm'); const data = getFormData(form) console.log(data) // Send the data back serverside. } // Function to collect form data into an object function getFormData(form) { let formData = {}; for (let i = 0; i < form.elements.length; i++) { const element = form.elements[i]; if (element.name) { if (element.type === "checkbox" || element.type === "radio") { formData[element.name] = element.checked; } else { formData[element.name] = element.value; } } } return formData; } </script> </body> </html> |
Example 2: Multiple time fields
What if we want to have multiple instances of the custom time input in our HTML?
In this example, we will have a start and end time. Notice that we will need to change the name each time slightly so that the value is stored.
As such, in the example we append -start
and -end
to the input fields.
Yeap, that’s all you need to do.
Check out the updated code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 |
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- For the tutorial: A Custom-Built HTML Time Input Element https://yagisanatode.com/a-custom-build-html-time-input-element/ --> <title>Time</title> </head> <body> <style> .timeInput { display: grid; grid-template-columns: repeat(7, auto); width: 8.0em; } .timeInput input::-webkit-outer-spin-button, .timeInput input::-webkit-inner-spin-button { display: none; margin: 0; } .timeInput .time-lables { font-size: x-small; text-align: center; } .timeInput .time-hours { width: 1.2em; text-align: center; } .timeInput .time-minutes { width: 1.2em; text-align: center; } .timeInput .time-seconds { width: 1.2em; text-align: center; } .timeInput .time-milliseconds { width: 2em; text-align: center; } </style> <form id="inputForm"> <div>Start Time</div> <div class="timeInput"> <span class="time-lables">HH</span> <span class="time-lables"></span> <span class="time-lables">MM</span> <span class="time-lables"></span> <span class="time-lables">SS</span> <span class="time-lables"></span> <span class="time-lables">MS</span> <input class="time-hours" type="number" name="time-hours-start" min=0 max=24 step=1 placeholder="00" data-maxlen=2> <span>:</span> <input class="time-minutes" type="number" name="time-minutes-start" min=0 max=60 step=1 placeholder="00" data-maxlen=2> <span>:</span> <input class="time-seconds" type="number" name="time-seconds-start" min=0 max=60 step=1 placeholder="00" data-maxlen=2> <span>:</span> <input class="time-milliseconds" type="number" name="time-milliseconds-start" min=0 max=999 step=1 placeholder="000" data-maxlen=4> </div> <hr> <div>Start Time</div> <div class="timeInput"> <span class="time-lables">HH</span> <span class="time-lables"></span> <span class="time-lables">MM</span> <span class="time-lables"></span> <span class="time-lables">SS</span> <span class="time-lables"></span> <span class="time-lables">MS</span> <input class="time-hours" type="number" name="time-hours-end" min=0 max=24 step=1 placeholder="00" data-maxlen=2> <span>:</span> <input class="time-minutes" type="number" name="time-minutes-end" min=0 max=60 step=1 placeholder="00" data-maxlen=2> <span>:</span> <input class="time-seconds" type="number" name="time-seconds-end" min=0 max=60 step=1 placeholder="00" data-maxlen=2> <span>:</span> <input class="time-milliseconds" type="number" name="time-milliseconds-end" min=0 max=999 step=1 placeholder="000" data-maxlen=4> </div> <hr> </form> <button onclick="submitForm()">Submit</button> <script type="text/javascript"> /** * Handles the time values for the timeInput element. * - Provides validation * -- Only numbers * -- witin min and max * -- max length * - Moves cursor to next input after max length is exceeded. */ function customTimeHandler() { /** * Validates the input value. * @param {HTMLElement} input */ function validate(input) { let val = input.value // Check for non numbers & remove val = val.replaceAll(/\D/g, "") // If greater than data-maxlen, remove last char val = val.substring(0, input.dataset.maxlen) const valNum = Number(val) // If greater than max, set max if (valNum < input.min) { val = input.min } // If less than min, set min else if (valNum > input.max) { val = input.max } // If less than total number of chars in the placeholder add leading zero const placeholderZeroes = input.placeholder const charLenMax = 10 ** (input.placeholder.length - 1) if (valNum == 0) { val = placeholderZeroes } else if (valNum < charLenMax) { const lenDif = placeholderZeroes.length + 1 - val.length const addedZeros = placeholderZeroes.substring(1, lenDif) val = addedZeros + val } input.value = val } const timeInputElements = document.querySelectorAll(".timeInput") timeInputElements.forEach(timeInputEl => { const timeInputs = timeInputEl.querySelectorAll('input') timeInputs.forEach((input, idx) => { input.addEventListener('focus', function() { input.value = "" }) input.addEventListener('input', function() { const inputLen = input.value.length const maxLen = input.dataset.maxlen if (inputLen >= maxLen) { validate(input) if (idx < timeInputs.length - 1) { timeInputs[idx + 1].focus() } } }) input.addEventListener('focusout', function() { validate(input) }) }) }) } customTimeHandler() /** * ##### SUBMIT FORM ##### * */ // Function to handle form submission function submitForm() { const form = document.getElementById('inputForm'); const data = getFormData(form) console.log(data) // Send the data back serverside. } // Function to collect form data into an object function getFormData(form) { let formData = {}; for (let i = 0; i < form.elements.length; i++) { const element = form.elements[i]; if (element.name) { if (element.type === "checkbox" || element.type === "radio") { formData[element.name] = element.checked; } else { formData[element.name] = element.value; } } } return formData; } </script> </body> </html> |
Example 3: Build Multiple HTML Custom Time Input fields dynamically with JavaScript
In this final example, we want to dynamically create time input fields equal to the number of tests we have. We will do this with the createTimeHanlder()
function.
Next, we will add the HTML for the custom time input into a template literal (A string with inputs) and then add a name and append a counter to the name tags of each number input element.
Here’s the updated code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 |
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- For the tutorial: A Custom-Built HTML Time Input Element https://yagisanatode.com/a-custom-build-html-time-input-element/ --> <title>Time</title> </head> <body> <style> .timeInput { display: grid; grid-template-columns: repeat(7, auto); width: 8.0em; } .timeInput input::-webkit-outer-spin-button, .timeInput input::-webkit-inner-spin-button { display: none; margin: 0; } .timeInput .time-lables { font-size: x-small; text-align: center; } .timeInput .time-hours { width: 1.2em; text-align: center; } .timeInput .time-minutes { width: 1.2em; text-align: center; } .timeInput .time-seconds { width: 1.2em; text-align: center; } .timeInput .time-milliseconds { width: 2em; text-align: center; } </style> <form id="inputForm"> <div id="target_timeInputs"> </div> </form> <button onclick="submitForm()">Submit</button> <script type="text/javascript"> function createTimeFields(number){ let htmlData = "" for(i= 0; i < number; i++){ const num = i + 1 const name = `Test ${num}` const appendName = `-${num}` const timeFields = ` <div>${name}</div> <div class="timeInput"> <span class="time-lables">HH</span> <span class="time-lables"></span> <span class="time-lables">MM</span> <span class="time-lables"></span> <span class="time-lables">SS</span> <span class="time-lables"></span> <span class="time-lables">MS</span> <input class="time-hours" type="number" name="time-hours${appendName}" min=0 max=24 step=1 placeholder="00" data-maxlen=2> <span>:</span> <input class="time-minutes" type="number" name="time-minutes${appendName}" min=0 max=60 step=1 placeholder="00" data-maxlen=2> <span>:</span> <input class="time-seconds" type="number" name="time-seconds${appendName}" min=0 max=60 step=1 placeholder="00" data-maxlen=2> <span>:</span> <input class="time-milliseconds" type="number" name="time-milliseconds${appendName}" min=0 max=999 step=1 placeholder="000" data-maxlen=4> </div> <hr> ` htmlData += timeFields } const targetElement = document.getElementById("target_timeInputs") targetElement.innerHTML = htmlData customTimeHandler() } createTimeFields(4) /** * Handles the time values for the timeInput element. * - Provides validation * -- Only numbers * -- witin min and max * -- max length * - Moves cursor to next input after max length is exceeded. */ function customTimeHandler() { /** * Validates the input value. * @param {HTMLElement} input */ function validate(input) { let val = input.value // Check for non numbers & remove val = val.replaceAll(/\D/g, "") // If greater than data-maxlen, remove last char val = val.substring(0, input.dataset.maxlen) const valNum = Number(val) // If greater than max, set max if (valNum < input.min) { val = input.min } // If less than min, set min else if (valNum > input.max) { val = input.max } // If less than total number of chars in the placeholder add leading zero const placeholderZeroes = input.placeholder const charLenMax = 10 ** (input.placeholder.length - 1) if (valNum == 0) { val = placeholderZeroes } else if (valNum < charLenMax) { const lenDif = placeholderZeroes.length + 1 - val.length const addedZeros = placeholderZeroes.substring(1, lenDif) val = addedZeros + val } input.value = val } const timeInputElements = document.querySelectorAll(".timeInput") timeInputElements.forEach(timeInputEl => { const timeInputs = timeInputEl.querySelectorAll('input') timeInputs.forEach((input, idx) => { input.addEventListener('focus', function() { input.value = "" }) input.addEventListener('input', function() { const inputLen = input.value.length const maxLen = input.dataset.maxlen if (inputLen >= maxLen) { validate(input) if (idx < timeInputs.length - 1) { timeInputs[idx + 1].focus() } } }) input.addEventListener('focusout', function() { validate(input) }) }) }) } /** * ##### SUBMIT FORM ##### * */ // Function to handle form submission function submitForm() { const form = document.getElementById('inputForm'); const data = getFormData(form) console.log(data) // Send the data back serverside. } // Function to collect form data into an object function getFormData(form) { let formData = {}; for (let i = 0; i < form.elements.length; i++) { const element = form.elements[i]; if (element.name) { if (element.type === "checkbox" || element.type === "radio") { formData[element.name] = element.checked; } else { formData[element.name] = element.value; } } } return formData; } </script> </body> </html> |
Conclusion
That’s it. I would love to hear how you used this in your own projects and what modifications you made. Let me know in the comments.
I am actually using this for a project for a Google Workspace Add-on using Google Apps Script along with an accompanying hosted private site using Golang, SQLite, temple and HTMX.
Have fun!
If you have found the tutorial helpful, why not shout me a coffee ☕? I'd really appreciate it.