Introduction
Part of my job as a software engineer working for a startup involves building proof of concept (POC) web apps quickly, and one of my preferred tech stacks for such a project is a Next.js application in conjunction with the Ant Design component library.
I'm a fan of Ant Design because it's a library I'm familiar with, and it offers a lot of more complicated components (think filterable tables, nested trees, complex forms, date pickers, and so on) that are highly customizable and just work right out of the box.
When I was tasked recently with building a web app that contained a time slider akin to the adjustable sliders you see in interactive weather maps or other time series data applications that would allow a user to adjust the visible time range within a predetermined set of dates, because I couldn't find a suitable, React-based component or package already available, I ended up modifying a basic Ant Design slider component to suit my needs.
My time slider component modifies its tick mark date formatting appropriately based on how big or small the original time span is, it allows users to drag the slider from either end to adjust the currently selected date range, it allows for date ranges set in the future, and it even delineates on the slider between past dates and future dates with different colors and styling.
Today, I'll show you how to build your own custom time slider inside of a Next.js application using the Ant Design component and the Day.js date formatting library.
Here's what the final adjustable time slider component looks like and a link to the live demo site in CodeSandbox.
Install Ant Design and Day.js
The two libraries you'll need to add to a React-based application to build this custom time range slider are the Ant Design library and the Day.js library.
Run this command in the terminal in the root of your project to add them both.
npm install antd dayjs
As I said before, Ant Design is a great, full-featured, very well documented component library I've worked with many times in the past and it makes building complex web apps that contain elements like drawers, cards, cascading selects, and a host of other components much quicker and easier to get going.
Ant Design uses Day.js by default, and Day.js is a lightweight, JavaScript library that parses, validates, manipulates, and displays dates and times with a largely Moment.js-compatible API. If you've ever used Moment to handle dates in a JavaScript application, you'll feel right at home with Day.js, with the added bonus that it's a fraction of the size of Moment.
Ant Design Slider
One particularly useful component that Ant Design offers is a <Slider />
component with a lot of customizable functionality not many other sliders contain.
For my app, I needed a slider that could:
- Select a range within the slider's bound (instead of being anchored to one end of the slider or the other),
- Allow that range track to be dragged within the slider.
- Offer customizable tick marks, tooltips, and range track styles.
Day.js
Working with dates in JavaScript applications is still quite the pain, even today without a supporting date library. The Day.js library is a faster, smaller alternative to Moment.js, an extremely popular library for handling dates and times, with a very similar API and great internationalization support.
To make dates work for my app with the Ant Design slider, I needed a library that could:
- Transform human readable dates like "2024-12-01" into Unix millisecond timestamps, because the slider expects numbers (not date formatted objects) to be passed to it in order to work correctly
- Format those same dates appropriately for tick marks of varying date spans
- Format said date for hoverable tooltips as well.
The Day.js library is capable of all this and more with simple, easy to understand functions and great documentation, and since it's the default library for Ant Design anyway, it make sense to just fall in line with what Ant D knows best.
Create a TimeSlider component
Once those two libraries are installed in the project, it's time to move on to actually creating the <TimeSlider />
component. Below is the starting code for the component. I'll go through all the pieces that comprise it afterwards.
import { Col, Row, Slider } from "antd";
import dayjs, { Dayjs } from "dayjs";
type TimeSliderProps = {
outerDateRange: { from: Dayjs; upto: Dayjs };
innerDateRange: { from: Dayjs; upto: Dayjs };
onChange: (innerDateRange: { from: Dayjs; upto: Dayjs }) => void;
};
const TimeSlider = ({
outerDateRange,
innerDateRange,
onChange,
}: TimeSliderProps) => {
const min = outerDateRange.from.valueOf();
const max = outerDateRange.upto.valueOf();
const values: [number, number] = [
innerDateRange.from.valueOf(),
innerDateRange.upto.valueOf(),
];
return (
<div>
<Row>
<Col span={24}>
<Slider
range={{ draggableTrack: true }}
min={min}
max={max}
value={values}
onChange={(value: number[]) => {
onChange({
from: dayjs(value[0]),
upto: dayjs(value[1]),
});
}}
/>
</Col>
</Row>
</div>
);
};
export default TimeSlider;
I created a new file called TimeSlider.tsx
where all this code is going to live, and made a basic shell of the component that accepts the props outerDateRange
, innerDateRange
, and onChange()
from the parent component. Later on in this article I'll show you how to set up the required state and functions in the parent component to pass down to this component.
As this is actually a TypeScript file, this component also gets its own TimeSliderProps
types defined for each of the props being passed to it. This is where the Day.js
library initially comes into play as well, as the type for outerDateRange
, innerDateRange
, and even the onChange()
function expect Day.js dates to be passed to them.
NOTE: All of my JavaScript-related code is actually written in TypeScript, so if you prefer to use JavaScript, just know that it can be fairly easily adapted to plain JS by removing things like types from the components and variable declarations.
Inside of this component we're going to set a bunch of variables required by the Ant <Slider />
component to render properly.
- A
min
andmax
variable to tell the slider what it's beginning and ending values are. - A
values
object that is an array of theinnerDateRange
'sfrom
andupto
values.
NOTE: For the range slider to work as you'd expect with date objects, the dates must be converted to Unix Epoch time, that is why you'll notice both the
outerDateRange
andinnerDateRange
dates are being called with the Day.js library'svalueOf()
function. This takes the date and turns it into a Unix Timestamp in milliseconds, which can then be passed to the<Slider/>
component.
This is the bare minimum of data the Ant Design component will need to actually render onscreen. Then, it's time to create the component with the help of the Ant Design <Slider/>
element and pass the necessary details to it.
For a bit of nicer formatting, I also employed Ant Design's <Col/>
and <Row/>
elements, but those are not required for this component to work. They help the component to lay out in a horizontal fashion and take up as much space as the screen width will allow.
The <Slider/>
component itself requires the min
and max
variables, the values
array, the onChange()
function, and in order to make it a range slider whose track can be dragged, the range={{draggableTrack: true}}
value must be added to the slider's props.
If this component were to receive valid dates right now, it should render, albeit without much to see. Let's keep going.
Calculate and style the tick marks to handle different date ranges appropriately
The next step is to calculate the tick marks that will appear along the slider's track showing the range of the slider.
Here's the code that will get added to the TimeSlider.tsx
component to make that happen.
import { Col, Row, Slider } from "antd";
import { SliderRangeProps } from "antd/lib/slider";
import dayjs, { Dayjs } from "dayjs";
type TimeSliderProps = {
outerDateRange: { from: Dayjs; upto: Dayjs };
innerDateRange: { from: Dayjs; upto: Dayjs };
onChange: (innerDateRange: { from: Dayjs; upto: Dayjs }) => void;
};
const TimeSlider = ({
outerDateRange,
innerDateRange,
onChange,
}: TimeSliderProps) => {
const min = outerDateRange.from.valueOf();
const max = outerDateRange.upto.valueOf();
const tickCount = 15;
const tickInterval = Math.floor((max - min) / tickCount);
const values: [number, number] = [
innerDateRange.from.valueOf(),
innerDateRange.upto.valueOf(),
];
const fmt = (date: number) => {
const d = dayjs(date);
const { from, upto } = outerDateRange;
const range = dayjs(upto).diff(dayjs(from)); // Calculate time difference in milliseconds
// Pick an appropriate level of granularity for the date range we're viewing
if (range < 1000 * 60 * 60 * 24) return d.format("hh:mm A"); // range is less than a day, show hours and minutes
if (range < 1000 * 60 * 60 * 24 * 7) return d.format("M/D, HH:mm"); // range is less than a week, show day of the week
if (range < 1000 * 60 * 60 * 24 * 30) return d.format("M/D"); // range is less than a month, show month / day
if (range < 1000 * 60 * 60 * 24 * 365) return d.format("MMM D"); // range is less than a year, show month and day
if (range < 1000 * 60 * 60 * 24 * 365 * 10) return d.format("MMM YYYY"); // range is more than a year, show year and month
return d.format("YYYY"); // range is more than 10 years, show year
};
const marks: Record<number, { label: string; style?: React.CSSProperties }> =
{
[min]: { label: fmt(min) },
[max]: { label: fmt(max) },
};
// add ticks to marks
for (let i = min + tickInterval; i <= max; i += tickInterval) {
marks[i] = {
label: fmt(i),
};
}
return (
<div>
<Row>
<Col span={24}>
<Slider
range={{ draggableTrack: true }}
min={min}
max={max}
marks={marks}
value={values}
onChange={(value: number[]) => {
onChange({
from: dayjs(value[0]),
upto: dayjs(value[1]),
});
}}
/>
</Col>
</Row>
</div>
);
};
export default TimeSlider;
First a couple of new variables are introduced at the top of the component: tickCount
and tickInterval
.
-
tickCount
is the number of ticks that should be displayed on the slider. -
tickInterval
is a value calculated by determining how far away from each other themin
andmax
dates are in time and then dividing that difference by thetickCount
number to figure out how to evenly space the ticks along the slider, regardless of what the actual date values are.
Then, I created a function called fmt()
which handles the heavy lifting of formatting the dates associated with the tick marks appropriately based on how far apart in time the dates actually are.
const fmt = (date: number) => {
const d = dayjs(date);
const { from, upto } = outerDateRange;
const range = dayjs(upto).diff(dayjs(from)); // Calculate time difference in milliseconds
// Pick an appropriate level of granularity for the date range we're viewing
if (range < 1000 * 60 * 60 * 24) return d.format("hh:mm A"); // range is less than a day, show hours and minutes
if (range < 1000 * 60 * 60 * 24 * 7) return d.format("M/D, HH:mm"); // range is less than a week, show day of the week
if (range < 1000 * 60 * 60 * 24 * 30) return d.format("M/D"); // range is less than a month, show month / day
if (range < 1000 * 60 * 60 * 24 * 365) return d.format("MMM D"); // range is less than a year, show month and day
if (range < 1000 * 60 * 60 * 24 * 365 * 10) return d.format("MMM YYYY"); // range is more than a year, show year and month
return d.format("YYYY"); // range is more than 10 years, show year
};
The fmt()
function takes in a date (which will eventually be a tick mark), translates it to a Day.js date (d
) that can be easily formatted as needed, figures out the range between between the min
and max
dates passed into the component from its parent, and then formats the d
date according to how wide the total timeline range
is.
If range
is less than a day, the tick marks will be styled to show hours and minutes, if the range
is less than a week, it will show day of the week plus hours and minutes, and so on as the range
increases. It takes a little bit of effort to decide when it makes sense to switch from one tick display format to the next, but it ends up working out pretty well in practice. Feel free to adjust your formatting of different date ranges to suit your needs.
Next I created the marks
that will get passed to the Ant Design slider component.
These marks
are created in two parts.
// marks for each end of the time slider component
const marks: Record<number, { label: string; style?: React.CSSProperties }> =
{
[min]: { label: fmt(min) },
[max]: { label: fmt(max) },
};
// add ticks to marks
for (let i = min + tickInterval; i <= max; i += tickInterval) {
marks[i] = {
label: fmt(i),
};
}
The first half of the code, where const marks
is declared, is where the marks for each end of the slider are created. Each end's date is passed to the fmt()
function to have it correctly rendered based on the length of the time span.
{
[min]: { label: fmt(min) },
[max]: { label: fmt(max) },
};
Then, each tick between the two ends is run through the formatting function as well via this for
loop.
for (let i = min + tickInterval; i <= max; i += tickInterval) {
marks[i] = {
label: fmt(i),
};
}
Finally, the resulting marks
array is passed to the <Slider/>
component, along with all the other values covered earlier.
<Slider
range={{ draggableTrack: true }}
min={min}
max={max}
marks={marks}
value={values}
onChange={(value: number[]) => {
onChange({
from: dayjs(value[0]),
upto: dayjs(value[1]),
});
}}
/>
Format the tooltip
After marks for the timeline, it's time to format the tooltip when a user hovers over either end of the range slider. This is actually one of the easier bits of code within this component as well.
Inside of the component under where all the variables are declared at the top add the following function.
import { SliderRangeProps } from "antd/lib/slider";
const TimeSlider = ({
outerDateRange,
innerDateRange,
onChange,
}: TimeSliderProps) => {
// variables declared up here
const formatter: NonNullable<SliderRangeProps["tooltip"]>["formatter"] = (
value: string | number | undefined
) => `${value ? dayjs(value).format("MM/DD/YYYY HH:mm A") : ""}`;
This formatter()
function takes in the current value of the end of the range slider a user is hovering over, and reformats it to a nicely readable date.
Then call this formatter()
function within the <Slider/>
component at the bottom of the component file.
<Slider
range={{ draggableTrack: true }}
min={min}
max={max}
marks={marks}
value={values}
onChange={(value: number[]) => {
onChange({
from: dayjs(value[0]),
upto: dayjs(value[1]),
});
}}
tooltip={{ formatter }}
/>
Voila! Nicely formatted tooltips for the time range slider on hover.
Style the track and marks to handle current and future dates differently
Now for the extra credit additions to this time slider component: styling future dates differently within the same component. Sounds impossible? I thought so too, at first, but now I can assure you it's not.
There's two changes I wanted to implement to help users know when they were looking at dates in the past/present versus dates in the future with the time slider:
- I wanted to change the color of the track from Ant Design's light blue default to a different color, and
- I wanted to change change the color of tick marks in the future, as well.
Lucky for me, Ant Design allows for immense customization of its components, even down to things as specific as track and tick mark styling.
The track styling was more complicated, so let's tackle that one first.
import dayjs, { Dayjs } from "dayjs";
const TimeSlider = ({
outerDateRange,
innerDateRange,
onChange,
}: TimeSliderProps) => {
// other variables declared at top of component here
const current = dayjs().valueOf();
// Calculate track styles based on the current date
const getTrackStyle = () => {
if (values[1] <= current) {
// All within past/present
return {
backgroundColor: "#91caff",
};
} else if (values[0] >= current) {
// All in future
return {
backgroundColor: "rgb(165, 222, 129)",
};
} else {
// Split between past/present and future
const pastWidthPercentage =
((current - values[0]) / (values[1] - values[0])) * 100;
return {
background: `linear-gradient(
to right,
#91caff 0%,
#91caff ${pastWidthPercentage}%,
rgb(165, 222, 129) ${pastWidthPercentage}%,
rgb(165, 222, 129) 100%
)`,
};
}
};
return (
<div>
<Row>
<Col span={24}>
<Slider
range={{ draggableTrack: true }}
min={min}
max={max}
marks={marks}
value={values}
styles={{
track: getTrackStyle(),
}}
onChange={(value: number[]) => {
onChange({
from: dayjs(value[0]),
upto: dayjs(value[1]),
});
}}
tooltip={{ formatter }}
/>
</Col>
</Row>
</div>
);
};
export default TimeSlider;
In order to get the track to change color midway through (for when it straddled date ranges both in the past and the future), I needed to declare a new variable called current
to keep track of what the actual current date is.
At the top of the component where the other variables are declared, current
is added to the mix, and it's simply calling the Day.js library's valueOf()
function) to get the current date as a Unix timestamp.
Once the current date is identified, I made a function called getTrackStyle()
that compares the values captured by either end of the draggable date range slider and sets the color of the track accordingly.
When both slider values are less than or equal to the current time, the entire track is styled light blue, when both slider values are greater than current time (i.e. the future), the entire track is styled light green. When the range spans both past and future dates, the function creates a gradient effect relying on the pastWidthPercentage()
function to determine where to transition the colors and the CSS linear-gradient to create a split color effect.
I'll be honest with you: ChatGPT helped me with this track styling magic. It's been extremely helpful for many complex things I've needed to do with Ant Design components, and I'd recommend working with it if you get stuck.
The getTrackStyle()
function was passed to the <Slider/>
component inside its styles
property as styles={{ track: getTrackStyle() }}
.
Then, I styled the tick marks based in the future in a different color as well.
for (let i = min + tickInterval; i <= max; i += tickInterval) {
const futureDate = i > current;
marks[i] = {
label: fmt(i),
style: futureDate
? {
color: "rgb(73, 163, 15)",
}
: {},
};
}
To accomplish this, I modified the for
loop that went through all the predetermined ticks and compared each one to the current
variable declared above. If any of the marks were in the future, those marks got a little extra styling applied to them in the form of a green font color. If the marks were not in the future, they retained their original font color.
Bonus section: I also wanted a "Now" tick mark to clearly delineate where the time slider track went from blue dates in the past/present to green dates in the future.
const marks: Record<number, { label: string; style?: React.CSSProperties }> =
{
[min]: { label: fmt(min) },
[current]: {
label: "Now",
style: {
color: "rgb(89, 147, 251)",
transform: "translate(-50%, -36px)",
fontWeight: "bold",
},
},
[max]: { label: fmt(max) },
};
To add an extra tick mark entitled "Now" along my time slider track, I modified the marks
variable, adding in the current
variable as one more pre-defined tick mark, and giving it a label of "Now" and a little styling to stand apart from the rest of the tick marks along the track.
The "Now" tick mark only shows up when the dates passed to the time slider actually cross the current date, but I think it aids in understanding of what's going on onscreen.
With all of that done, the time slider component itself is complete, and it's time to add it to a parent component that will pass it the dates and functions it requires.
Import the TimeSlider component into the parent page
Ok, now it's time to give the custom <TimeSlider/>
component the props it needs.
In my case, I had other components that also needed to be aware of any changes to the date range for display purposes (I had chart and map child components that needed to be updated as well when selected dates changed), so I had a page within my Next.js app holding the date state variables and onChange()
function that were passed to the <TimeSlider/>
component.
Inside of the index.tsx
file within the pages/
folder, I added the following code:
import { useState } from "react";
import dayjs, { Dayjs } from "dayjs";
import TimeSlider from "@/components/TimeSlider";
import styles from "@/styles/MainContent.module.css";
type HomeProps = {
startDate: string;
endDate: string;
};
export default function Home({ startDate, endDate }: HomeProps) {
const [initialDate, setInitialDateRange] = useState({
startDate: dayjs(startDate),
endDate: dayjs(endDate),
});
const [selectedDate, setSelectedDateRange] = useState({
startDate: dayjs(startDate),
endDate: dayjs(endDate),
});
const handleDateSliderChange = (innerDateSpan: { from: Dayjs; upto: Dayjs }) => {
setSelectedDateRange({
startDate: innerDateSpan.from,
endDate: innerDateSpan.upto,
});
};
return (
<>
<main className={styles.mainContent}>
<div className={styles.page}>
<h2 style={{ textAlign: "center", margin: "20px auto 0 auto" }}>
Custom Ant Design Next.js Time Slider
</h2>
<div className={styles.timeSlider}>
<TimeSlider
outerDateRange={{
from: initialDate.startDate,
upto: initialDate.endDate,
}}
innerDateRange={{
from: selectedDate.startDate,
upto: selectedDate.endDate,
}}
onChange={handleDateSliderChange}
/>
</div>
</main>
</>
);
}
export async function getStaticProps() {
const startDate = dayjs("2024-12-01").utc().format("YYYY-MM-DDTHH:mm:ss[Z]");
const endDate = dayjs("2025-02-28").utc().format("YYYY-MM-DDTHH:mm:ss[Z]");
return {
props: {
startDate,
endDate,
},
};
}
In the index.tsx
page, I created three variables related to the <TimeSlider/>
component.
- The
initialDate
andselectedDate
React states, which are both objects containing astartDate
andendDate
property. - The
handleDateSliderChange()
function, which accepts newstartDate
andendDate
values, and updates theselectedDate
state accordingly.
For convenience, I chose to set my start and end date values inside the Next.js getStaticProps()
function call which runs before the page renders in the browser, but it's not required, and I set both date state objects equal to the defined dates.
The initialDate
values (which end up determining the min and max ends of the time slider won't change again until they are modified in getStaticProps()
and the page is refreshed), but the selectedDate
values are free to change within those bounds thanks to the handleDateSliderChange()
function which will update the state of the selectedDate
values whenever the user interacts with the <TimeSlider/>
component.
The <TimeSlider/>
component was imported into the index.tsx
file and the initialDate
object was passed as its outerDateRange
prop, the selectedDate
object was passed as its innerDateRange
prop, and the handleDateSliderChange()
function was passed as its onChange()
prop.
At this point, the component should have everything it needs to work.
Test out the Time Slider component
If you'd like to see a working demo of this <TimeSlider />
component in action, I've got a CodeSandbox demo for you to try out.
Open a terminal and type npm run dev
to get it started.
In addition to the time slider component, I also included a few nice extras: some Ant Design date picker components and buttons with values like "Last month", "Last week", and "Last day" to show other ways of manipulating the selectedDate
state controlled by the parent component.
If you'd like to see how the time slider adjusts to larger or smaller date ranges or play around with past and future dates, just adjust the dates inside the getStaticProps()
function in the index.tsx
file and refresh the browser.
Conclusion
As I was building a React-based proof of concept web app that needed a time slider that users could drag and adjust to zoom in or out on particular pieces of data over time, I couldn't find a ready-made solution for such a need. So I built one myself.
Using the Ant Design component library's highly customizable <Slider/>
component as the base and manipulating dates with the help of the Day.js library, I was able to build a flexible, full-featured, and decent looking time slider without a whole lot of extra code on my end.
Then I took it a few steps further making the slider handle future dates and style them differently and even display a little "Now" tick mark when the slider crossed over dates in the past to dates in the future dates. It's a pretty specific use case, I know, but I think there's more possible applications for interesting sliders like this once you start to think beyond the typical date time options.
Check back in a few weeks — I’ll be writing more about JavaScript, React, IoT, or something else related to web development.
If you’d like to make sure you never miss an article I write, sign up for my newsletter here: https://paigeniedringhaus.substack.com
Thanks for reading. I hope if you need a way to control time series data views within an app and don't want to rely on just date pickers or inputs, you'll consider giving this solution a try. Enjoy!
Top comments (0)