How To Build A Killer Custom Time Input

by kleamerkuri
How to build custom time input.

I was on the lookout for a custom time input design for an upcoming THT project when I chanced upon this particular baby by Roman Kamushken.

It seems the design is part of a React UI kit but the details don’t concern me. I found what I was looking for 😏

But hold up – why bother creating a custom time input, you ask?

Picture this: You’re browsing a website and need to select a specific time, but the default time input is dull and totally out of character from the rest of the site.

So much for maintaining a cohesive brand design 😔

While HTML provides a default time input type, it is limited in terms of customization and varies according to the browser in use.

Not only will your web app have a basic input, but it’ll also be a changeling.

Frustrating, right?

Well, fear not because today I’ll show you how to create a custom time input using HTML, CSS, and JavaScript that will make your users actually enjoy selecting a time.

In the end, we’ll have something like this:

See the Pen Time Input by Klea Merkuri (@thehelpfultipper) on CodePen.

With just a few lines of code, you can add personality and functionality to your interface, making your website truly your own.

So, let’s get creative and bring some joy to our user’s day with a custom time input field!

HTML: Structuring the custom time input

A logical question that comes to mind when looking at the reference design is how in the world you build that out.

In DM’s words, what does the HTML of that thing look like? 🤨

In the usual programming sense, there’s no one answer to that question because the structure is largely limited by imagination and ingenuity.

To me, the design is a grouping of three input fields that are styled in a side-by-side fashion.

<div id="time_wrapper">
  <div id="time_input">
    <label for="hours">
      <input type="number" id="hours" value="0">
      <span class="label lbl-hrs">hours</span>
    </label>
    <span>:</span>
    <label for="minutes">
      <input type="number" id="minutes" value="00">
      <span class="label lbl-min">minutes</span>
    </label>
    <span>:</span>
    <label for="seconds">
      <input type="number" id="seconds" value="00">
      <span class="label lbl-sec">seconds</span>
    </label>
  </div>
</div>

I’m going the extra leg of including a label. This feature isn’t part of the design but, in my esteemed opinion, totally should be for accessibility.

Of course, my esteemed self has to find a way to work the label into the design 🤦‍♀️ We’ll worry about that later.

For now, we’ve simply created three input fields of a number type. Each input stands for hours, minutes, and seconds, respectively.

Notice none of the inputs are a type of time! If you designate them with a type of time, default browser-specific styles will apply.

Default HTML time input type on Chrome.
Default HTML time input type on Chrome.

In turn, each designated time input has a default value set using the HTML value attribute. Then I wrap each input in its corresponding label.

Read: HTML For Beginners – A Step-by-Step Guide To HTML Tags

Each label is separated by a colon string and that sums up the main structure of the custom input.

Custom time input HTML structure.

CSS: Adding styles to the custom input

First, I opted to remove the input’s default styles by designating a type of number to the input itself.

Second, I give stylistic structure to the wrappers using position and display. Then proceed to size each of the inputs.

:root {
  --inactive: #acd4ae;
  --active: #2b662e;
}

body {
  font-family: sans-serif;
}

#time_wrapper {
  width: fit-content;
  margin: 0 auto;
  /*   background: pink; */
  position: relative;
  top: 150px;
  transform: scale(1.5);
}

#time_input {
  border: 2px solid var(--inactive);
  width: fit-content;
  color: #86c288;
  display: flex;
  align-items: center;
}

input {
  width: 60px;
  height: 60px;
  border: none;
  box-sizing: border-box;
  padding: auto 15px;
  text-align: center;
  color: #132c14;
}

Tip: Setting custom CSS variables is a tremendous help when you’re dealing with recurring properties like colors. Check out An Easy Way To Create A Custom Input Dropdown for custom CSS variables in action!

Adding stylistic structure to custom time input.

Next, let’s move on to the label. I decided to have the label placed above the time display and to do this I’ll use a flex display to stack the input and label.

label {
  display: flex;
  flex-direction: column;
  justify-content: center;
  position: relative;
}

.label {
  font-size: 0.55rem;
  position: absolute;
  top: 4.5px;
}

.label.lbl-hrs {
  left: 20px;
}

.label.lbl-min {
  left: 14px;
}

.label.lbl-sec {
  left: 13px;
}

The rest of the label code targets each input label to place appropriately relative to the wrapper and each input.

Styling and positioning the input labels.

Third, get rid of the default browser increment control buttons that are available to an input with a type of number.

I’m talking about this guy 👇 that appears once hovering over, or clicking, on the input:

Default number type input controls.
/* Chrome, Safari, Edge, Opera */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

/* Firefox */
input[type="number"] {
  -moz-appearance: textfield;
}

#time_input,
input {
  border-radius: 8px;
  font-size: 1.5rem;
}
Defined focus state for input.

Lastly, define the styles of the custom time input when in a focused state when a valid, as well as when an invalid, input is provided.

Note: Validation of the inputs will occur in the JavaScript step. The invalid class will apply based on the logic we define.

input:focus {
  outline: 3px solid var(--active);
}

input:focus + .label {
  color: var(--active);
}

input.invalid:focus {
  outline: 3px solid red;
}

input.invalid:focus + .label {
  color: red;
}
Complete custom time input styles.

Our custom time input is looking rather dashing but it doesn’t walk the talk if you get my meaning 😏

So far, the custom time input looks like the reference design (at least, sans the labels 👉 👈) yet it doesn’t act like a time input.

JavaScript: Validating the custom time input

There are certain things we don’t want the custom time input to do, such as allowing a user to enter non-digits or go over a certain min and max value.

Adding validation to custom time input.

We’ll start by creating a validation formula for the inputs.

1) Prevent user from entering non-digits

I’ll declare a validInput()function that prevents users from entering non-digits in the input fields.

The function takes in a value and runs it through a regex pattern to replace any non-digits with an empty string. That’s equivalent to removing the non-digits.

// variables
const inputs = document.querySelectorAll("input");

// functions
const validInput = (val) => {
  // prevent user from inserting non-digits
  return val.replace(/[^0-9]+/g, "");
};

To execute validInput(), loop over the inputs array listening for a keyup event on each input.

When the event occurs, get the value of the target input, run it through validInput(), and set the input’s value equal to the function’s returned value.

// actions
inputs.forEach((input) => {

  input.addEventListener("keyup", (e) => {
    let val = e.target.value;

    // only allow digits
    input.value = validInput(val);

  });

});

Remember validInput() returns only the non-digits of whatever was entered in the input field!

Since a negative sign ( – ) is not a digit, the character gets caught by the regex pattern which means validInput() also prevents negative numbers!

2) Set max value for each input

Typically we can set max and min values for an input with a type of number using the max and min HTML attributes.

However, some of the built-in validation using the min and max attributes is no longer available due to overriding their appearance.

As the next step, we’ll control for min and max values.

And, luckily for us, we’re already handling the minimum since nothing less than zero works thanks to validInput().

Incremental values, like decimals, are also not allowed because of the regex expression in validInput().

Recall that a fullstop ( . ) is not considered a digit!

So all that’s left is to handle the max value 💁‍♀️

One way to avoid extremely large numbers, such as those more than two characters long, is by a simple conditional check.

// restrict input characters
    if(e.target.value.length > 2) {
      input.value = e.target.value.substring(0,2);
    }

The above check goes into the keyup event listener callback and prevents any numbers over 99 from getting entered.

Next, I’ll define the logic of the max value in a conditional function.

const setMax = (input) => {
  let id = input.getAttribute("id"),
    max = 0;
  switch (id) {
    case "hours":
      max = 23;
      break;
    default:
      max = 60;
  }

  return max;
};

The setMax() function takes an input and grabs its id to check whether the input is the one that shows hours or not.

If the input is that for hours, the max is going to be 23 while for minutes and seconds, it’ll be 60.

I’m using a switch statement as an alternative to an if-conditional – it’s totally up to you what to use!

How to apply the max value using setMax()

Next up, we’ll define the logic to set the max value for each input.

  1. Store the max of the input in a variable, let max = setMax(input) .
  2. Check the value of the input is greater than max to apply the invalid class.
// set max value allowed
    if (+input.value > max) {
      input.classList.add("invalid");
    } else {
      input.classList.remove("invalid");
    }

3) Displaying an error message when input is invalid

Going a step further, I’d like to display an error message to the user about what exactly is going on.

Simply changing colors isn’t a guaranteed indicator for clearly communicating an error no matter how universal red marks may be 😌

I’ll start by adding a container for the error message: <div id="error"></div> .

Then position the error container on the page making sure visibility is set to hidden.

#error {
  width: 280px;
  position: relative;
  top: 170px;
  margin: 0 auto;
  color: red;
  visibility: hidden;
}

Note: Hiding the error container isn’t that important since it’s by default empty. But going off the assumption this time input is embedded in a web page with other content, having empty containers around can affect spatial distribution.

Afterward, I’ll define a function that takes a message, an input, and a control variable (aka the invalid boolean) to control the error display.

const throwErr = (mssg, input, invalid=false) => {
  // control display of error
  if(invalid === true) {
    input.classList.add("invalid");
    error.style.visibility = "visible";
    error.innerHTML = `<small>${mssg}</small>`;
  } else {
    input.classList.remove("invalid");
    error.style.visibility = "hidden";
    // error.innerHTML = "";
  }
}

Call throwErr() inside the max value conditional using a control variable, isInvalid , and a variable to store the error message.

// set max value allowed
    if (+input.value > max) {
      // handle error
      isInvalid = true;
      // e.target.classList.add('invalid');
      mssg = `It can't be more than ${max} ${input.getAttribute(
        "id"
      )}! Try again.`;

      throwErr(mssg, input, isInvalid);
    } else {
      isInvalid = false;
      throwErr(mssg, input);
    }

Notice that the error message is explicitly tailored to an input. I’m using the target input’s ID for displaying the units – hours, minutes, or seconds – since I happened to assign the IDs accordingly.

4) Controlling which error message displays

Our time input is coming along nicely what with are dandy extra additions like error messages.

But there’s a slight problem with our current error displays that are best demonstrated in the following little vid.

Stop scrolling and zone your gaze to the error message in the visual above.

See how clicking on other inputs doesn’t have an effect on the last error message that displays on the screen.

Even though I click on the minutes and hours inputs, the error for the seconds input is what you see.

This is not the intended behavior 🫢

We want the error message to:

  1. Keep displaying if the value of the input is not corrected (i.e. the input is invalid)
  2. Show the correct error message for the particular input we’re focused on

We’re handling the first point but the second requires a new event listener, one listening to the focus event rather than the keyup.

input.addEventListener("focus", (e) => {
    // Blank input on default values
    if(e.target.value === "0" || e.target.value === "00") {
      e.target.value = "";
    } 

    // Show error mssg for invalid inputs when not corrected
    if(e.target.classList.contains('invalid')) {
      isInvalid = true;
      throwErr(mssg, input, isInvalid);
    } else {
      isInvalid = false;
      throwErr(mssg, input);
    }
  });

In the focus event listener callback, I do two things:

  1. clear the value of the input from the defaults when a user clicks an input
  2. show the correct error message, if it exists, for a particular input

With that, the custom input project is complete!

In little time, we’ve created time inputs that are both functional and visually appealing. Now, this is a project that’s absolutely not a waste of anyone’s time 😏

Grab the code on GitHub and start building ✌️

Leave a Comment

* By using this form you agree with the storage and handling of your data by this website.

Related Posts