← Back toDrag and drop in React

Create a time picker

Written byPhuoc Nguyen
Created
21 Nov, 2023
Tags
React time picker
A time picker lets users choose a specific time or range of times. It's especially handy in scheduling applications such as appointment booking or calendars. Users can select hours, minutes, and AM/PM, and some even include seconds and time zones. This makes it easy to input accurate and consistent scheduling information.
Time pickers are used in many applications, like calendar apps that let you schedule events at specific times. Booking systems, like hotel reservations or appointments, rely on time pickers to ensure resources are available when needed. Time tracking software lets you record time spent on a project and often includes a time picker for accuracy. E-commerce websites may use time pickers for selecting delivery times or scheduling pickups.
By providing a simple and intuitive way to input specific times, time pickers improve user experience and accuracy. In this post, we'll explore how to create a time picker with React.

A new approach to choosing hours and minutes

Usually, users are given two select inputs to choose hours or minutes. But in this post, we'll take a different approach. Users can now choose hour or minutes by simply dragging elements. They can drag a small circle along the boundary of an outer circle that represents an analog clock to adjust hours or minutes.
In our previous post, we explored how to drag an element along a circle. And to make this functionality reusable, we created a custom hook. Using this hook is incredibly simple: all you need to do is call a function.
ts
const [draggableRef, dx, dy] = useDraggable();
The function we're using returns an array with three items. The first item is the reference of the target element, which we can attach using the `ref` attribute. The last two items show how far the element has been moved, specifically the distance between the top-left corner of the element and its container. We can use these values to position the element by passing them to the `translate` function.
To refresh your memory, here's the code snippet we used earlier.
tsx
<div className="container">
<div
className="draggable"
ref={draggableRef}
style={{
transform: `translate(${dx}px, ${dy}px)`,
}}
/>
</div>
Please take a look at the demo below:
There's a problem with the positioning of an element at the start, and once users move the element into the circle boundary, they can't tell the current value. To fix these issues, we can add a new parameter to the hook, like `initialAngle`, which sets the starting angle. Then the updated hook can return the current angle based on the current position.
Here's an example of what the updated hook might look like:
ts
const useDraggable = ({ initialAngle }) => {
return [ref, dx, dy, angle];
};
Calculating the angle of our draggable element is easy with basic trigonometry. We just need to measure the angle between the center of the container and the current position of the element.
ts
const radians = Math.atan2(dy - center, dx - center);
const angle = (radians + Math.PI / 2) / (2 * Math.PI);
setAngle(angle > 0 ? angle : 1 + angle);
In our time picker example, we want the top of the circle to represent a 0 degree angle. To achieve this, we can add an offset of `Math.PI / 2` to the radians calculation and divide it by `2 * Math.PI`. This gives us an angle value between 0 and 1, where 0 represents the top position of the circle. We then check if the angle is greater than 0, and if not, we add 1 to it. This ensures that we always have a value between 0 and 1 for our angle, no matter where the user starts dragging from. With this update, users can interact with the circle in a way that feels natural and intuitive.
To calculate the initial position of a draggable element based on the provided `initialAngle`, we can use the `useEffect` hook. First, we get the width of both the draggable element and its container using `getBoundingClientRect()`. Then, we calculate the radius of the circle by dividing the container width by 2. After that, we determine the center point of the circle by finding half of the difference between the container width and the draggable element width.
Next, we use trigonometry to calculate `dx` and `dy` values based on the initial angle, using `Math.cos()` and `Math.sin()` functions. Finally, we update our state with the new values for `dx` and `dy`, so our draggable element starts at a specific position on our circular path.
Here's an example of how you can implement this:
ts
React.useEffect(() => {
const width = node.getBoundingClientRect().width;
const containerWidth = node.parentElement.getBoundingClientRect().width;
const radius = containerWidth / 2;
const center = radius - width / 2;

const radians = initialAngle * Math.PI * 2 - Math.PI / 2;
const dx = center + radius * Math.cos(radians);
const dy = center + radius * Math.sin(radians);

setOffset({ dx, dy });
}, [node]);
By using the `useEffect` hook in this way, we can set the starting position of our draggable element based on a preferred angle along a circular path.

Adjusting hours

To adjust hours in 0-12 format, we'll make use of the `useDraggable` hook we created earlier. In this example, we'll create a reusable component that takes in the `initialHours` prop.
tsx
const TimePicker = ({ initialHours }) => {
...
};
To establish an initial value for the hours on a circular path, we can pass an `initialAngle` prop that represents the draggable element's starting position.
To calculate the `initialAngle` from the `initialHours` prop, we divide it by 12, giving us a value between 0 and 1. This value represents the percentage of hours that have passed during the day. For instance, if `initialHours` is set to 6, half of the day has passed, and the initial position of the draggable element should be at 0.5 along the circular path. By converting hours to a percentage value, we can easily convert it to an angle value that works with our draggable element.
ts
const [draggbleHoursRef, dxHours, dyHours, angleHours] = useDraggable({
initialAngle: initialHours / 12,
});
As usual, you can use the first three values returned by the hook to render the drag hours.
tsx
<div
ref={draggbleHoursRef}
style={{
transform: `translate(${dxHours}px, ${dyHours}px)`,
}}
/>
We can use `Math.round` and a bit of math to convert the current angle of the draggable element into a corresponding hour value between 1 and 12. But to keep things consistent with standard timekeeping conventions, we need to adjust this value so that 0 represents 12 (midnight) instead of 0.
ts
const hours = Math.round(angleHours * 12);
const normalizedHours = hours === 0 ? 12 : hours;
Check out the demo below:

Adjusting minutes

We can take a similar approach to allow users to adjust the minutes on our app. By passing a new prop called `initialMinutes`, we can use the `useDraggable` hook to set the initial angle based on the starting minute. This way, users can easily adjust the minutes on the clock face.
ts
const TimePicker = ({ initialHours, initialMinutes }) => {
const [draggbleMinutesRef, dxMinutes, dyMinutes, angleMinutes] = useDraggable({
initialAngle: initialMinutes / 60,
});
});
To calculate the `initialAngle` from the `initialMinutes` prop, we first divide it by 60 to get a value between 0 and 1. This value represents the percentage of minutes that have passed during an hour. For example, if `initialMinutes` is set to 30, it means half an hour has passed, and the initial position of the draggable element should be at 0.5 along the circular path. By converting minutes to a percentage value, we can easily convert it to an angle value that works with our draggable element.
Now, our component has two draggable elements: one for changing the hours and the other for changing the minutes. Since only one of them is active at a time, we can create a TypeScript enum to determine which one is currently selected. This will help us keep track of which element we need to update based on user input.
ts
enum Selection {
Hours = 'Hours',
Minutes = 'Minutes',
};
The component internally manages the selected handler through a state:
ts
const [selection, setSelection] = React.useState(Selection.Hours);
To show which element is currently selected, we need to update the selection state when the user clicks on either the hours or minutes button. We can do this by adding an event handler to each button that sets the selection state based on which button was clicked.
tsx
<button onClick={() => setSelection(Selection.Hours)}>
{normalizedHours}
</button>
<span>:</span>
<button onClick={() => setSelection(Selection.Minutes)}>
{normalizedMinutes}
</button>
In this example, clicking on the "Hours" button sets the selection state to `Selection.Hours`, while clicking on "Minutes" sets it to `Selection.Minutes`. This allows us to keep track of which draggable element is currently active and display it accordingly.
To show or hide the hours dragging handler, we simply adjust its `opacity` based on the `selection` state. When `Selection.Hours` is active, we set the `opacity` of the hours dragging handler to 1, making it fully visible. Conversely, when `Selection.Minutes` is active, we set the `opacity` of the hours dragging handler to 0, making it fully transparent. This way, users can easily see which element they're currently interacting with.
tsx
<div
ref={draggbleHoursRef}
style={{
transform: `translate(${dxHours}px, ${dyHours}px)`,
opacity: selection === Selection.Hours ? 1 : 0,
}}
/>
We can use opacity to show or hide elements based on user interaction, without affecting their layout or position. It's a good practice to use different styles for hours and minutes dragging handlers, like a distinct background color, so users can easily distinguish which one they're interacting with.
Go ahead and try out the demo below. Start by dragging the small circle to change the hours. Then, click on the minutes to activate the minutes dragging handler.

Conclusion

To sum up, we've just explored how to create a customizable time picker using React. By using the `useDraggable` hook, we were able to create draggable elements that follow a circular path. We also learned how to convert angles into time values and vice versa, which allows users to easily adjust hours and minutes on our clock face.
This component can be further customized with additional features like ticks along the circle for better precision or custom styling options. Armed with the knowledge gained from this post, you can build more complex UI components that require draggable elements along circular paths.
If you found this post helpful, please consider giving the repository a star on GitHub or sharing the post on your favorite social networks 😍. Your support would mean a lot to me!

Questions? 🙋

Do you have any questions about front-end development? If so, feel free to create a new issue on GitHub using the button below. I'm happy to help with any topic you'd like to learn more about, even beyond what's covered in this post.
While I have a long list of upcoming topics, I'm always eager to prioritize your questions and ideas for future content. Let's learn and grow together! Sharing knowledge is the best way to elevate ourselves 🥷.
Ask me questions

Recent posts ⚡

Newsletter 🔔

If you're into front-end technologies and you want to see more of the content I'm creating, then you might want to consider subscribing to my newsletter.
By subscribing, you'll be the first to know about new articles, products, and exclusive promotions.
Don't worry, I won't spam you. And if you ever change your mind, you can unsubscribe at any time.
Phước Nguyễn