Introduction
If you are into online learning/education or an unboxing video of a gadget on youtube, etc. Then I am pretty sure you might have come across this thing here:

The progress bar is broken into sections and each section describes what it represents. This is mostly done by the video's author so that viewers can quickly get a gist of what they will be learning in this section or the next few minutes.
These quick gist of each section are called chapters. In this article, we will be looking at how to implement them.
So without further ado, let us get started.
Prerequisites
- What are VTTs? My previous Blog post, MDN
- Project Architecture: Previous Post: In depth or you can read the summary
- Slider component: Previous post: How I build a YouTube Video Player with ReactJS: Build the Seekbar control
- Understanding of track element.
- Understanding of Javascript’s map function, editing element attributes.
- React:
useImperativeHandle
hook
The What?
So this broken time bar/seek bar/ progress bar that you see on the video is nothing but chapters. This is a very common feature that you get to see on YouTube.
We already have built the seek bar in our previous blog posts and with some tweaks to the existing Slider component we can achieve this functionality.
The Why?
We are building this component because it:
- Tells the user what is happening in the given time frame.
- Gives a quick reference to the content the user is looking for with the help of chapter text below the frame snapshot.
- Provides a better User experience.
The How?
Before we get started with the implementation let us have a quick overview of what the steps are:
- Generate VTT for chapters
- Load the VTT into the video with the help of
track
element. - Extract chapters from the
track
element. - Update the Slider component for the chapter logic
- Display the chapters in the tooltip
There isn’t much jargon here if you have gone through the previous article. If not, I would highly recommend going through it from the prerequisites section.
💡 NOTE: The implementation of chapter fill css-variable was inspired from the https://www.vidstack.io/
Generating VTT for chapters
Web video text track is a file format that has time-based data. Meaning, that it contains text that needs to be shown on the video along with the information of when this text needs to be shown(nth second).
Since we want the users to know from which second to which second the chapter starts and ends we will write a VTT file like the one below:
WEBVTT
00:00:00.000 --> 00:00:15.000
Chapter 1
00:00:15.000 --> 00:00:42.000
Chapter 2
00:00:42.000 --> 00:00:48.000
Chapter 3
00:00:48.000 --> 00:01:01.000
Chapter 4
00:01:01.000 --> 00:01:09.000
Chapter 5
This tells us that, from the 0th to the 15th second there would be text that corresponds to Chapter 1 and so on.
Load the VTT into the video
Next, to load the VTT into the video we make use of the [track
element.](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/track) We update the Video.tsx
like below:
<video onTimeUpdate={handleTimeUpdate} ref={videoRef} crossOrigin="" width={800}> | |
<source src={videoSourceInfo.src} type="video/mp4" /> | |
<track ref={trackMetaDataRef} default kind="metadata" src="<path-to-image-sprite-vtt" /> | |
<track ref={trackChaptersRef} default kind="chapters" src="path-to-chapters-vtt" /> | |
</video> |
Here we added a track element with kind attribute of chapters. In the src attribute make sure that you place the location/url of the chapter’s vtt
file that we generated above.
Extracting chapters from the track
element
Now that we have track
element in place let us make use of it to extract the chapters into our code. What we want here is that we need these chapters when the video is loaded completely.
To do that, we already have a useEffect
in the Video.tsx
file that hydrates the store with some parameters. We make use of this same effect to capture the chapters. Copy and paste below useEffect
:
useEffect(() => { | |
const video = videoRef.current; | |
if (video) { | |
video.addEventListener("loadeddata", () => { | |
const payload: OnLoadType = { | |
hasVideoLoaded: true, | |
totalDuration: video.duration, | |
}; | |
if (trackChaptersRef.current) { | |
const { track } = trackChaptersRef.current; | |
const cues = track.cues; | |
if (cues) { | |
payload["chapters"] = (Object.values(cues) as VTTCue[]).map((cue: VTTCue, index) => ({ | |
index, | |
chapterName: cue.text, | |
endTime: cue.endTime, | |
startTime: cue.startTime, | |
percentageTime: (((cue.endTime - cue.startTime) / video.duration) * 100).toFixed(2), | |
})); | |
} | |
} | |
dispatch({ | |
type: HAS_VIDEO_LOADED, | |
payload, | |
}); | |
}); | |
} | |
return () => { | |
video && | |
video.removeEventListener("loadeddata", () => { | |
console.log("Loadeddata event removed"); | |
}); | |
}; | |
}, []); |
Here we make use of the video element’s loadeddata
event to hydrate the store. The code is pretty self-explanatory but I would like to explain the part of the chapter here:
- In line 12, we make use of
trackChaptersRef
which is being passed to the track element. From this, we access thetrack
object. - In line 13, we get all the cues in the track.
- On the next line, we make use of the
map
function to return an array of objects that consists of:- index
- name of the chapter, start and end time
-
percentageTime
: This represents what percentage of time this chapter contributes to out of the total video duration.
I know this hydration of chapter object with additional fields doesn’t make any sense right now but it will when we get into the changes of the slider component. We will discuss this in the next section.
Update the Slider component for the chapter logic
Folks, take a cup of coffee now since this will be a long section. I will try my best to make it interesting 🙂.
Before proceeding further, I would highly recommend going through this Slider component implement article.
Here is the outline of the steps that we will be doing to achieve the chapter functionality:
- Split the Slider component into multiple mini-slider components
- Handling Slider fill across the multiple mini-sliders
- Handling mouse moves on hover, click and drag.
- Updating the overall slider on playback
- Styling the slider
Split the Slider component into multiple mini-slider components
Ok so hear me out:
Chapters on Slider is nothing but a bunch of mini sliders.
From this what I mean is, in this functionality, we won’t have a single slider of width 100%. But instead, we will have mini sliders of length equal to the width of percentageTime
. This state is the derived state that we computed when we loaded the video component in the above section.
We got the jest of what we are doing, now let us start with the implementation.
- The first thing that we need to do is, extend our Slider component such that it accepts chapters as a prop:
- Let us update the
SliderProps
interface:This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersinterface SliderProps extends Omit< React.HTMLAttributes<HTMLDivElement>, "onClick" | "onDrag" | "onMouseUp" | "onMouseMove" > { $total: number; $shouldDisplayChapters?: boolean; $currentTime?: number; //=============== here ================ $currentChapter?: Chapter; $chapters?: Chapter[]; $fillColor?: string; //=============== onClick?: (currentPercentage: number) => void; onDrag?: (completedPercentage: number) => void; onMouseUp?: () => void; onMouseMove?: (pointerPercentage: number) => void; } export interface SliderRefProps { updateSliderFill: (completedPercentage: number) => void; updateChapterFill: ( currentChapterIdx: number, completedPercentage: number, ) => void; } - Update the render method of the slider component such that when chapters are enabled we map over them and create multiple sliders:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
return ( <StyledContainer className="slider" $hasChapters={hasChapters} $fillColor={$fillColor} onMouseMove={handleContainerMouseMove} onMouseUp={handleContainerMouseUp} ref={rootRef} $total={$total} > {hasChapters ? ( $chapters?.map((chapter, index) => ( <StyleChapterContainer className={`chapter-${index}`} key={`key-${chapter.percentageTime}`} ref={(el: HTMLDivElement) => el && (chapterRefs.current[index] = el)} $width={`${chapter.percentageTime}%`} > <StyledTrack className="slider-track" onClick={handleClick} /> <StyledSliderFill className="slider-fill" $hasChapters /> </StyleChapterContainer> )) ) : ( <> <StyledTrack className="slider-track" onClick={handleClick} /> <StyledSliderFill className="slider-fill" $hasChapters={false} /> </> )} <StyledThumb className="slider-thumb" onMouseDown={handleThumbMouseDown} ></StyledThumb> </StyledContainer> ); - We also maintain references to all these mini sliders with the help of chapterRefs. It is an array ref that holds the reference to these elements. We do this in this line: ```
- Let us update the
ref={(el: HTMLDivElement) => el && (chapterRefs.current[index] = el)}
- Quick thing to note here, We have created a StyleChapterContainer. It may look like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type StyledChapterContainerProps = {
$width: string;
};
const StyleChapterContainer = styled.div<StyledChapterContainerProps>`
width: ${(props) => props.$width};
height: 5px;
display: inline-block;
margin-right: 2px;
position: relative;
transition: height 200ms ease;
`;
- It accepts a prop as `$width` that defines the width of this mini - slider component.
-
Now pass the chapters to the slider component via the seekbar component as follows:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<Slider
$chapters={chapters}
$currentTime={currentTime}
$total={780}
$fillColor="#ff0000"
onClick={onPositionChangeByClick}
onDrag={onPositionChangeByDrag}
onMouseUp={handleMouseUp}
onMouseMove={handleMouseMove}
ref={sliderRef}
/>;
Once this is hooked the output will look like below
Handling filling logic on mouse move and click events
The above logic needs to be handled for the following scenarios:
- While clicking and,
- While dragging the slider
Here is the pictorial representation of these scenarios:
- While Clicking:
<p>
<figure>
<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/erymo9rbzpjls48q4cw5.gif" alt="Click interaction on sliders with chapters" />
<figcaption>Click interaction on sliders with chapters</figcaption>
</figure>
</p>
- While dragging:
<p>
<figure>
<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g8ryc9ia89pw6gc3bvo6.gif" alt="Drag interaction on sliders with chapters" />
<figcaption>Drag interaction on sliders with chapters</figcaption>
</figure>
</p>
<aside>
💡 NOTE: I would like to remind you guys that to go through this blogpost: How I build a YouTube Video Player with ReactJS: Build the Seekbar control before proceeding further. Because it contains a lot of jargon which is better explained there.
</aside>
So here we go, below is the handleClick
event handler that takes care of fill the chapters on click:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (rootRef.current) {
/**
* Algorithm:
* 1. We first get the value from slider fill.
* 2. Then we loop over the chapers and take the sum with previous chapters fill width and see if its less than slider fill.
* 3. If yes, then we completely fill the chapter with 100% width
* 4. If no, then we calculate the chapter-fill for that particular chapter.
*/
updateSliderFillByEvent("--slider-fill", e);
const width = getCSSVariableAbsoluteValue("--slider-fill", rootRef);
if ($chapters) {
const allChapterWidths = $chapters.map((chapter) => Number(chapter.percentageTime));
let acc = 0;
const currentChapterIdx = allChapterWidths.findIndex((val) => {
acc += val;
if (acc > width) {
return true;
}
});
const nextIdx = currentChapterIdx === $chapters.length - 1 ? $chapters.length - 1 : currentChapterIdx + 1;
const prevChapterIdx = currentChapterIdx === 0 ? 0 : currentChapterIdx - 1;
const prevChapterElem = chapterRefs.current[prevChapterIdx];
const currentChapterElem = chapterRefs.current[currentChapterIdx];
const nextChapterElem = chapterRefs.current[nextIdx];
// Fill the previous elements:
for (let i = 0; i < currentChapterIdx; i++) {
chapterRefs.current[i].style.setProperty("--chapter-fill", "100%");
}
const previousChapterFill = getCSSVariableAbsoluteValue("--chapter-fill", prevChapterElem);
const currentChapterFill = getCSSVariableAbsoluteValue("--chapter-fill", currentChapterElem);
const nextChapterFill = getCSSVariableAbsoluteValue("--chapter-fill", nextChapterElem);
// Fill the current chapter;
if (previousChapterFill === 100 && currentChapterFill >= 0) {
const currentChapterWidth = allChapterWidths[currentChapterIdx];
const rect = currentChapterElem.getBoundingClientRect();
const totalChapterWidth = (currentChapterWidth * $total) / 100;
const chapterFillWidth = computeCurrentWidthFromPointerPos(e.pageX, rect.left, totalChapterWidth);
currentChapterElem.style.setProperty("--chapter-fill", `${chapterFillWidth}%`);
}
// clean up the later chapters when going from right to left;
if (nextChapterFill > 0) {
for (let i = currentChapterIdx + 1; i < $chapters.length; i++) {
chapterRefs.current[i].style.setProperty("--chapter-fill", "0%");
}
}
}
onClick?.(width);
}
};
Below is the explanation for the same:
- This part right here calculates all the chapter's width and with the help of the current
--slider-fill
value we get the current chapter index:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const allChapterWidths = $chapters.map((chapter) => Number(chapter.percentageTime));
let acc = 0;
const currentChapterIdx = allChapterWidths.findIndex((val) => {
acc += val;
if (acc > width) {
return true;
}
});
- Next, we get the previous and the next chapter elements:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const nextIdx =
currentChapterIdx === $chapters.length - 1 ? $chapters.length - 1 : currentChapterIdx + 1;
const prevChapterIdx = currentChapterIdx === 0 ? 0 : currentChapterIdx - 1;
const prevChapterElem = chapterRefs.current[prevChapterIdx];
const currentChapterElem = chapterRefs.current[currentChapterIdx];
const nextChapterElem = chapterRefs.current[nextIdx];
- Next, we fill all the previous chapters before the current chapter:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Fill the previous elements:
for (let i = 0; i < currentChapterIdx; i++) {
chapterRefs.current[i].style.setProperty("--chapter-fill", "100%");
}
A thing to note here, we track the fill of each slider element with the help of the CSS variable: --chapter-fill
.
We also need to update our StyledSliderFill
component such that whenever chapters exist we make use of the —chapter-fill
CSS variable.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const StyledSliderFill = styled.div<StyledSliderFillProps>`
height: 5px;
background-color: var(--slider-fill-color);
width: var(${(props) => (props.$hasChapters ? "--chapter-fill" : "--slider-fill")}, 0%);
position: absolute;
pointer-events: none;
`;
- Next, we summon all the chapter fill values below:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const previousChapterFill = getCSSVariableAbsoluteValue("--chapter-fill", prevChapterElem);
const currentChapterFill = getCSSVariableAbsoluteValue("--chapter-fill", currentChapterElem);
const nextChapterFill = getCSSVariableAbsoluteValue("--chapter-fill", nextChapterElem);
Then, to fill the current chapter we make use of the computeCurrentWidthFromPointerPos
utility function and then finally set its value to the current chapter’s --chapter-fill
.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Fill the current chapter;
if (previousChapterFill === 100 && currentChapterFill >= 0) {
const currentChapterWidth = allChapterWidths[currentChapterIdx];
const rect = currentChapterElem.getBoundingClientRect();
const totalChapterWidth = (currentChapterWidth * $total) / 100;
const chapterFillWidth = computeCurrentWidthFromPointerPos(e.pageX, rect.left, totalChapterWidth);
currentChapterElem.style.setProperty("--chapter-fill", `${chapterFillWidth}%`);
}
- For scenarios of clicking from right to left direction i.e. going back in video, we fill the next chapter to 0 from the current chapter:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// clean up the later chapters when going from right to left;
if (nextChapterFill > 0) {
for (let i = currentChapterIdx + 1; i < $chapters.length; i++) {
chapterRefs.current[i].style.setProperty("--chapter-fill", "0%");
}
}
Similarly, we make use of the handleMouseMove
function that handles the filling of chapters during the mousemove
event i.e. when we are dragging the slider knob:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const handleContainerMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
/**
* NOTE: 'data-dragging' attribute is available only when we are dragging the slider-thumb.
* Thumb is said to be dragged when onMouseDown gets triggered on the .slider-thumb -> [data-dragging] is added to .slider
* -> then handleMouseMove off .slider gets executed.
* The dragging of thumb gets stopped when the data-dragging attribute is removed from .slider
* We remove this attribute on MouseUp of .slider because on mouseup the target element can be different during dragging if the mouseup where on .slider-thumb
* To have better dragging experience the data-dragging is removed on the mouseup of .slider
*/
if (rootRef.current?.getAttribute("data-dragging")) {
updateSliderFillByEvent("--slider-fill", e);
const sliderFillWidth = getCSSVariableAbsoluteValue("--slider-fill", rootRef);
onDrag?.(sliderFillWidth);
// When chapters exists update the chapter fills for each div.
if ($currentChapter) {
const { index, percentageTime } = $currentChapter;
const currentChapterElem = chapterRefs.current[index];
const rect = currentChapterElem.getBoundingClientRect();
const totalChapterWidth = (Number(percentageTime) * $total) / 100;
const chapterFillWidth = computeCurrentWidthFromPointerPos(e.pageX, rect.left, totalChapterWidth);
/**
* Below if block removes the data-chapter-dragging attribute whenever the dragging happens from left to right or vice-versa;
*/
if ($currentTime && $chapters) {
// movement from left to right;
if (index > 0 && $currentTime >= $chapters[index].startTime) {
chapterRefs.current[index - 1].removeAttribute("data-chapter-dragging");
/**
* Here we update the chapter fill of the previous element since the previous element on
* complete wasn't getting completely filled i.e. around 98% or 97%.
* So to approximate this error we manually set the fill to 100%.
* Similar is the case when we are moving from right to left in the below if block
*/
chapterRefs.current[index - 1].style.setProperty("--chapter-fill", "100%");
}
if (index < $chapters.length - 1 && $currentTime <= $chapters[index].endTime) {
chapterRefs.current[index + 1].removeAttribute("data-chapter-dragging");
chapterRefs.current[index + 1].style.setProperty("--chapter-fill", "0%");
}
}
// Don't update the chapter-fill when it is beyond the limits
if (chapterFillWidth < 0 || chapterFillWidth > 100) {
return;
}
currentChapterElem.style.setProperty("--chapter-fill", `${chapterFillWidth}%`);
currentChapterElem.setAttribute("data-chapter-dragging", "true");
}
}
updateSliderFillByEvent("--slider-pointer", e);
const pointerPos = getCSSVariableAbsoluteValue("--slider-pointer", rootRef);
onMouseMove?.(pointerPos);
};
This is the same function we used to update the --slider-pointer
CSS variable in the previous blog posts. The purpose of this function from the chapters point of view is as follows:
- Update the chapter fill of the respective elements
- Add a
data-chapter-dragging
attribute
I would like you guys to go through the comments in the above code block to understand the functionality.
Just a quick note: The purpose of adding data-chapter-dragging
attribute is to use it in styling which we will look into in the later section.
Updating the slider position on playback
There is one more scenario left that we need to cover which is handling the playback mechanism across multiple sliders. Now to do this we need a mechanism that will help us to update the --chapter-fill
CSS variable of each chapter whenever the current duration of the video changes.
This is a simple task. We just need to expose a function that updates this chapter fill for the given chapter with the help of [useImperativeHandle
hook](https://react.dev/reference/react/useImperativeHandle).
The first thing is to update the useImperativeHandle
hook in the Slider
component:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
useImperativeHandle(
ref,
() => {
return {
updateSliderFill(percentageCompleted: number) {
rootRef.current?.style.setProperty("--slider-fill", `${percentageCompleted}%`);
},
updateChapterFill(currentChapterIdx: number, completedPercentage: number) {
chapterRefs.current[currentChapterIdx].style.setProperty("--chapter-fill", `${completedPercentage}%`);
},
};
},
[],
);
We introduced a new function here: updateChapterFill
which accepts the current chapter’s index and the percentage completion of that chapter.
Next, we consume this function via refs in the Seekbar
component like below:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const currentChapter = chapters?.filter((chapter) => currentTime && currentTime > chapter.startTime && currentTime < chapter.endTime);
useEffect(() => {
if (sliderRef.current && !isSeeking) {
const newPosPercentage = (currentTime / totalDuration) * 100;
sliderRef.current.updateSliderFill(newPosPercentage);
if (currentChapter.length > 0) {
const { index, endTime, startTime } = currentChapter[0];
const totalChapterDuration = endTime - startTime;
const currentChapterFillWidth = index === 0 ? (currentTime / totalChapterDuration) * 100 : ((currentTime - chapters[index - 1].endTime) / totalChapterDuration) * 100;
sliderRef.current.updateChapterFill(index, currentChapterFillWidth);
}
}
}, [currentTime, isSeeking]);
Inside the Seekbar
component, we access the currentTime
and the chapters
from the player context.
Then we make use of the useEffect
that updates on currentTime
and isSeeking
. Along with updating the slider-fill
it also updates the chapter fill with the help of updateChapterFill
.
We do some simple calculations and get the currentChapterFillWidth
by dividing the currentTime
by the total chapter duration.
lastly, we pass this to our update function.
This is how our functionality will look like when during playback:
<p>
<figure>
<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/whuawmq3nm8u8ucmy6nd.gif" alt="Chapter fill on video playback" />
<figcaption>Chapter fill on video playback</figcaption>
</figure>
</p>
Styling the chapter sliders
One last thing that is pending is adding a small styling effect that will give better UX feedback. The style that I am talking about is increasing the width of the slider whenever we are dragging the slider like below:
<p>
<figure>
<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iyb08j2kbzjpj4tt56qy.gif" alt="Emphasising chapters on drag" />
<figcaption>Emphasising chapters on drag</figcaption>
</figure>
</p>
To do this we make use of the data-chapter-dragging
attribute that we add in the handleMouseMove
function. We use this attribute in StyledContainer
of the Slider
component:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const StyledContainer = styled.div<StyledContainerProps>`
--slider-pointer: 0%; // when hover happens pointer is updated
--slider-fill: 0%; // when click and drag happens fill is updated
--slider-track-bg-color: ${COLORS.TRACK_BG_COLOR};
--slider-fill-color: ${(props) => props.$fillColor};
position: relative;
height: 30px;
width: ${(props) => props.$total};
display: flex;
flex-direction: row;
${(props) => props.$hasChapters && "justify-content: center;"}
align-items: center;
cursor: pointer;
// For animating ring behind the thumb;
&[data-dragging] {
& .slider-thumb::before {
opacity: 1;
}
// Increase the height of the chapter when dragging is enabled;
& [data-chapter-dragging] {
height: 8px;
}
}
// Make thumb visible when hovered on this container;
&:hover {
& .slider-thumb {
opacity: 1;
}
}
`;
We make sure that whenever we are dragging i.e. whenever the container has the data-dragging
attribute only then check for any child div that has data-chapter-dragging
attribute. If it has then we update the height of the slider/container by 8px
.
Summary
So to summarize we achieved the following things in this blogpost:
- We understand the reason why are we doing this feature.
- We saw how to get the chapter's data into the video via the track element.
- And lastly, we saw the changes that we need to make to the slider component to achieve the desired chapter UX.
The entire code for this tutorial can be found here.
Thank you for reading!
Top comments (0)