How I build a YouTube Video Player with ReactJS: Building the Volume Control
Tags: opensource, react, javascript, tutorial
Published: 2024-01-08 13:29:02

## Introduction

In this blog post, we are going to implement the volume control component. This blog is part of my **How I Build a YouTube Video Player with ReactJS** series do check it out [here](https://dev.to/keyurparalkar/building-with-react-js-create-your-own-youtube-video-player-starting-with-basics-1lpp). I do recommend you guys go through the series so that you can get the full grasp of this blog.

So without further ado let us get started.

## Prerequisites

Make sure to go through the previous blogs of this series along with the following topics:

- Typescript
- Advanced React concepts such as [useImperativeHandle](https://react.dev/reference/react/useImperativeHandle) hook
- styled-components
- CSS [pseudo-elements](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements) and [transition](https://developer.mozilla.org/en-US/docs/Web/CSS/transition) property.

## Building the VolumeControl structure

If you have followed the previous blogs from the series then you might know that we are building a control toolbar component. Each control performs a certain function that updates the video’s state. In this section, we will be implementing the `VolumeControl`.

The purpose of the `VolumeControl` is to provide two sub-controls:

- A button control that helps you to mute/unmute the video
- A slider that helps you control the volume of the video.

It acts as a container component for the above sub-controls. Here is what these sub-controls will look like:

![Volume Control Structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kydm0ebbgatv6smjac87.png)

To create the `VolumeControl` component, create a folder named `VolumeControl` inside the `components` folder and create a file named `index.tsx` in it. Paste the following piece of code in it:

```tsx
import styled from 'styled-components';
import MuteButton from './MuteButton';
import VolumeSlider from './VolumeSlider';

const StyledVolumeControl = styled.div`
    width: 20%;
    display: flex;
`;

const VolumeControl = () => {
    return (
        <StyledVolumeControl className="volume-control">
            <MuteButton />
            <VolumeSlider />
        </StyledVolumeControl>
    );
};

export default VolumeControl;
```
This is a simple component that Holds the `MuteButton` and `VolumeSlider` components.
 

> **NOTE**: Don’t try to run this code directly since we have not implemented the above two components

## Building the `MuteButton` sub-control component
Let us start building our first sub-control component which is the `MuteButton`. Create a file named `MuteButton.tsx` inside the `VolumeControl` folder and paste the following code in it:

```tsx
import { useContext } from 'react';
import styled from 'styled-components';
import { PlayerContext, PlayerDispatchContext } from '../../context';
import { ON_MUTE } from '../../context/actions';
import { StyledIconButton } from '../../utils';

type VolumeProps = {
    volume: number;
    muted?: boolean;
};

const StyledOuterBar = styled.path<VolumeProps>`
    opacity: ${(props) => (props.volume <= 0.5 ? 0 : 1)};
    transition: opacity 0.4s ease;
`;

const StyledInnerBar = styled.path<VolumeProps>`
    opacity: ${(props) => (props.volume <= 0.3 ? 0 : 1)};
    transition: opacity 0.4s ease;
`;

const StyledMutePath = styled.path<VolumeProps>`
    opacity: ${(props) => (props.volume < 0.09 || props?.muted ? 1 : 0)};
    d: ${(props) => (props.volume < 0.09 || props?.muted ? "path('M 1,1 L 45,50')" : "path('M 1,1 L 0,0')")};
    transition:
        d 0.4s ease,
        opacity 0.4s ease;
`;

const MuteButton = () => {
    const { muted, volume } = useContext(PlayerContext);
    const dispatch = useContext(PlayerDispatchContext);

    const handleMuteClick = () => {
        dispatch({
            type: ON_MUTE,
            payload: !muted,
        });
    };

    return (
            <StyledIconButton onClick={handleMuteClick} className="control--mute-button">
                <svg
                    stroke="currentColor"
                    fill="currentColor"
                    strokeWidth="0"
                    viewBox="0 0 45 24"
                    height="24px"
                    xmlns="http://www.w3.org/2000/svg"
                >
                    <StyledOuterBar
                        className="outer-bar"
                        d="M16 21c3.527-1.547 5.999-4.909 5.999-9S19.527 4.547 16 3v2c2.387 1.386 3.999 4.047 3.999 7S18.387 17.614 16 19v2z"
                        volume={volume}
                    />
                    <StyledInnerBar
                        className="inner-bar"
                        d="M16 7v10c1.225-1.1 2-3.229 2-5s-.775-3.9-2-5z"
                        volume={volume}
                    />
                    <path d="M4 17h2.697L14 21.868V2.132L6.697 7H4c-1.103 0-2 .897-2 2v6c0 1.103.897 2 2 2z" />
                    <StyledMutePath stroke-width="2" volume={volume} muted={muted} />
                </svg>
            </StyledIconButton>
    );
};

export default MuteButton;
```
There are two things that this `MuteButton` should take care of which are:

1. The volume bars should appear whenever the volume is increased and should disappear whenever the volume is decreased.
![volume_increase_decrease](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4lmay8a9pkw6a80iybr6.gif)
2. On this mute button it should show a backward slash whenever the volume turns to 0. This slash should also appear whenever the button is clicked. This signifies that the video is being muted and unmuted.

![On Click Interaction](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bpjt5sbfnixck1l5x25p.gif)

![When Volume Changes 0](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yzhgvznx2wxrsrx1x6ee.gif)

To achieve this we will make use of the [transition](https://developer.mozilla.org/en-US/docs/Web/CSS/transition) CSS property. We won’t go through all the sections of the above code since some of it is self-explanatory but we will look closely at how we will be doing this animation:
* First, we add the SVG element as mentioned in the above code. 
* Next, we make use of the styled-components to create a component `StyledOuterBar`. It will have all the attributes as mentioned in the above code it’s just that we are going to change the opacity with the following CSS:
```tsx
const StyledOuterBar = styled.path<VolumeProps>`
    opacity: ${(props) => (props.volume <= 0.5 ? 0 : 1)};
    transition: opacity 0.4s ease;
`;
```
So now we have an opacity CSS property that depends on the `volume` prop. The default opacity value is 1, but whenever the volume goes down below 0.5 then we tell the browser to turn the opacity to 0 but do it slowly i.e. with 0.4 seconds. This is what the line: `transition: opacity 0.4s ease;` does for us it tells the browser not to make this change immediately but to do it in this duration.
* We do the same thing for the inner bar. Take a look at the StyledInnerBar styled component.
* Next, to create the backward slash as mentioned above we again make use of the styled component and create a component: `StyledMutePath` with the following CSS:
```tsx
const StyledMutePath = styled.path<VolumeProps>`
    opacity: ${(props) => (props.volume < 0.09 || props?.muted ? 1 : 0)};
    d: ${(props) => (props.volume < 0.09 || props?.muted ? "path('M 1,1 L 45,50')" : "path('M 1,1 L 0,0')")};
    transition:
        d 0.4s ease,
        opacity 0.4s ease;
`;
```
* In this CSS, along with opacity we also add the `d` attribute to the transition CSS property. A thing to note here is that we have added the opacity property to the transition because we don’t want even the small dot that gets painted due to `L 0,0` in the `d` attribute to appear on the screen when the volume is less than `0.09`
* If you would like to learn more about how this path definition is created then I highly recommend you guys to read this [article](https://css-tricks.com/svg-path-syntax-illustrated-guide/).

## Building the Slider component

If we observe, any video player has a slider for its seek bar and another slider for changing the volume of the video. Here we see that a slider component can be regarded as a reusable component. Hence I have decided to create a common component named `slider`. This slider will be used for both controlling the volume and in the seekbar. 

Before we start jumping into building this component, let us first understand some terminologies that I have used in the component. This will help us understand this component clearly.

> **NOTE:** The below implementation is inspired by the video mentioned on the [vidstack.io](https://www.vidstack.io/) 


![Slider-component and its various parts](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xv0xv3tlqm3qj7os6m8g.png)

As you can see above the slider component is divided into multiple parts which are as follows:

- slider-fill
- slider-thumb
- slider-track

### Slider-track
This part of the slider represents the total width on which the progress will be happening. This part is generally in the lighter shade and acts as a base on which the slider will make progress. 

### Slider-fill
This represents the actual progress made on the slider. If 20% of the slider is consumed/completed then slider-fill represents that amount.

### Slider-thumb
A small knob is present in front of slider-fill that is used to increase and decrease the width of the slider-fill. This is an interactive element that helps to change the state of the slider component. The thumb is draggable as well as clickable along the slider-track component. We will look into this closely while we are implementing this component

## Working of slider-component

The working of this component is very simple. From above we know that slider-fill represents the current state of the slider for example, if a slider has a value of 50 that means it is 50% consumed and this is represented by showing slider-fill having a width of 50% of the slider track.

That’s right the current state of the component is set as the width of the slider-fill component which is equal to % width of the total slider-track.

While interacting with the knob we simply change the width of the slider-fill to represent the current state.

![slider-working](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rnhxdlkkliqdxj766rbc.png)

So in short, changing the position of the slider-thumb along the track determines the width of the slider-fill.

## Implementing the slider component

To implement this component we need to have the following functionalities:

- `div` elements that represent the above track, fill, and thumb components of the slider.
- On dragging of the thumb, the fill width should increase
- On click of the track, the position of the thumb along with the width of the fill should get updated to the current click’s location.

So let's implement these functionalities one by one by starting with implementing the basic structure of the slider with everything static no dragging or fancy stuff.

### Building the basic structure of the component

Let us start by creating a folder named `common` inside the `components` folder. Then create a new folder named `Slider` inside the common. Then create an `index.tsx` file for the same inside the `slider` folder.

Next, place the following context inside the newly created `index.tsx` file:

```tsx
const Slider = () => {
  return (
    <div className="slider">
      <div className="slider-track"></div>
      <div className="slider-fill"></div>
      <div className="slider-thumb"></div>
    </div>
  );
};

export default Slider;
```

Now let us add some styles to these `div`'s such that it will represent one static slider. To do that follow the below steps:

- Create a new styled component named: `StyledContainer`and have the following CSS for it:
```tsx
const StyledContainer = styled.div`
  position: relative;
  height: 30px;
  display: flex;
  flex-direction: column;
  justify-content: center;
`;
```
- Next, create another styled component named: `StyledTrack` with the following CSS for it:
    
    ```tsx
    const StyledTrack = styled.div`
        width: 100%;
        height: 5px;
        background-color: #cacaca;
        position: absolute;
        pointer-events: auto;
    `;
    ```
    
    - A thing to note here is that we enable the [pointer events](https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events) for the track because we want the entire slider to be clickable.

- Next, we create another styled component named: `StyleSliderFill` for the slider-fill component with the following CSS:
    
    ```tsx
    const StyledSliderFill = styled.div`
      height: 5px;
      background-color: red;
      width: 25%;
      position: absolute;
      pointer-events: none;
    `;
    ```
    
    - A thing to note here is that we disabled the pointer events for the fill because we wanted to prioritize the click events that were triggered for the track component rather than the fill component. Since the fill overlaps on the track component clicking on the fill would trigger the event from the fill component but this is what we want. We only want to click events from the track and hence we have added the `pointer-events: none`.

- Finally, we create our last styled component: `StyledThumb` for the slider-thumb component. Add the following CSS for it:
    
    ```tsx
    const StyledThumb = styled.div`
      height: 15px;
      width: 15px;
      border-radius: 50%;
      background-color: red;
      position: absolute;
      bottom: 35%;
      left: 25%;
      transform: translate(-50%, 15%);
    `;
    ```
    
    - A thing to note here is that we make use of `[translate](https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/translate)` to position the center of the thumb at the end of the fill component.

We now have all our styled components in place. It is time for us to use them in the component. Modify the `Slider` component such that it makes use of the above styled components:

```tsx
import styled from "styled-components";

const StyledContainer = styled.div`
  position: relative;
  height: 30px;
  display: flex;
  flex-direction: column;
  justify-content: center;
`;

const StyledTrack = styled.div`
  width: 100%;
  height: 5px;
  background-color: #cacaca;
  position: absolute;
  pointer-events: auto;
`;

const StyledSliderFill = styled.div`
  height: 5px;
  background-color: red;
  width: 25%;
  position: absolute;
  pointer-events: none;
`;

const StyledThumb = styled.div`
  height: 15px;
  width: 15px;
  border-radius: 50%;
  background-color: red;
  position: absolute;
  bottom: 35%;
  left: 25%;
  transform: translate(-50%, 15%);
`;

const Slider = () => {
  return (
    <StyledContainer className="slider">
      <StyledTrack className="slider-track"></StyledTrack>
      <StyledSliderFill className="slider-fill"></StyledSliderFill>
      <StyledThumb className="slider-thumb"></StyledThumb>
    </StyledContainer>
  );
};

export default Slider;
```

Once this is done, our output will look like below:

![static-slider](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mybzthbp75b4dqa7agxa.png)

Awesome, we now have a basic slider component. However, it is not functional. Let's make it functional as well.

### Making the Slider interactive

Interactivity in the slider component happens in two ways as discussed above:

1. Movement of the slider-thumb
2. On click of the slider-track

The interaction that happens does only one thing which is to change the state of the slider component. The state of the component is represented by the slider-fill’s width with respect to the slider-track.

So from this, we can conclude that dragging of slider thumb and on click of the slider track correlates to the change in the slider-fill’s width. 

We make use of the [CSS custom variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties) concept to manage the slider-fill’s width. I choose this approach because then I don’t need to maintain the width of the fill component in a state of the component but instead, I can just refer to it in any desired component that I need and use it.

We create 4 CSS custom variables that will be initialized in the `StyledContainer` of the `Slider`:

- `--slider-pointer`: This represents the percentage width at which the mouse is hovered on the slider-track.
- `--slider-fill`: Represents the slider-fill component’s width.
- `--slider-track-bg-color`: Track background color.
- `--slider-fill-color`: Slider-fill component’s background color.

We will take a closer look at all these custom variables in the later section of the blog post.

Let us now make the slider-thumb interactive.

### Make slider-thumb draggable

To make the slider-thumb draggable we make use of the `onMouseDown`, `onMouseMove` and `onMouseUp` event handlers. The dragging mechanism is very simple, we update the `left` CSS property of the slider-thumb whenever the mouse button is down and when the mouse is moving. 

We add the code of updating the `left` property in the `onMouseMove` handler but just adding this handler will cause the property to update even when the mouse button is not down. To avoid this we make sure that we add `data-dragging="true"` attribute to the `StyledContainer` component when the mouse is down. Once the mouse is up we remove this attribute. 

Inside the `mousemove` handler, we always check if the `StyledContainer` has the `data-dragging="true"` or not, if it has only then do we update the `left` property, or else we don’t update it.

> **NOTE:** I have chosen this approach rather than adding draggable="true" attribute to the slider-thumb element because this approach gives more flexibility in terms of customization. 

Now that we know what we need to do, let us start with its implementation:

- Create a reference to the `StyledContainer` as `rootRef` and attach it as a ref attribute to the container
```tsx
const Slider = () => {
  const rootRef = useRef<HTMLDivElement>(null);
  return (
    <StyledContainer className="slider" ref={rootRef}>
      <StyledTrack className="slider-track"></StyledTrack>
      <StyledSliderFill className="slider-fill"></StyledSliderFill>
      <StyledThumb className="slider-thumb"></StyledThumb>
    </StyledContainer>
  );
};
```

- Create a new function: `handleThumbMouseDown` and bind it to the `onMouseDown` event of the `StyledThumb`:
```tsx
const Slider = () => {
  const rootRef = useRef<HTMLDivElement>(null);

  const handleThumbMouseDown = () => {
    if (rootRef.current) rootRef.current.setAttribute("data-dragging", "true");
  };

  return (
    <StyledContainer className="slider" ref={rootRef}>
      <StyledTrack className="slider-track"></StyledTrack>
      <StyledSliderFill className="slider-fill"></StyledSliderFill>
      <StyledThumb
        className="slider-thumb"
        onMouseDown={handleThumbMouseDown}
      ></StyledThumb>
    </StyledContainer>
  );
}
```

- Next, create a new function: `handleContainerMouseUp`, and bind it to the `onMouseUp` event of the `StyledContainer`:
    
    ```tsx
    const Slider = () => {
      const rootRef = useRef<HTMLDivElement>(null);
    
      const handleContainerMouseUp = () => {
        if (rootRef.current) rootRef.current.removeAttribute("data-dragging");
      };
      const handleThumbMouseDown = () => {
        if (rootRef.current) rootRef.current.setAttribute("data-dragging", "true");
      };
    
      return (
        <StyledContainer
          className="slider"
          onMouseUp={handleContainerMouseUp}
          ref={rootRef}
        >
          <StyledTrack className="slider-track"></StyledTrack>
          <StyledSliderFill className="slider-fill"></StyledSliderFill>
          <StyledThumb
            className="slider-thumb"
            onMouseDown={handleThumbMouseDown}
          ></StyledThumb>
        </StyledContainer>
      );
    };
    ```
    
    - Note: Here we are removing the data-dragging attribute on the `mouseup` event of `StyledContainer` because we also need to handle scenarios where the user might be dragging the slider-thumb but the mouse pointer is not on the thumb but may be on the slider track or the entire container. So to provide an optimal experience the mouse up handler is added to the container

- We create another handler: `handleContainerMouseMove` and attach it to the `onMouseMove` event of the `StyledContainer` for the same reasons as above:
    
    ```tsx
    import { useRef } from "react";
    import styled from "styled-components";
    
    const StyledContainer = styled.div`
      --slider-fill: 0%; // <--------- 1
      position: relative;
      height: 30px;
      display: flex;
      flex-direction: column;
      justify-content: center;
      width: 680px;
    `;
    
    const StyledTrack = styled.div`
      width: 100%;
      height: 5px;
      background-color: #cacaca;
      position: absolute;
      pointer-events: auto;
    `;
    
    const StyledSliderFill = styled.div`
      height: 5px;
      background-color: red;
      width: var(--slider-fill, 0%); // <--------- 2
      position: absolute;
      pointer-events: none;
    `;
    
    const StyledThumb = styled.div`
      height: 15px;
      width: 15px;
      border-radius: 50%;
      background-color: red;
      position: absolute;
      bottom: 35%;
      left: var(--slider-fill, 0%); // <--------- 3
      transform: translate(-50%, 15%);
    `;
    
    // <------- 4
    const computeCurrentWidthFromPointerPos = (
      xDistance: number,
      left: number,
      totalWidth: number
    ) => ((xDistance - left) / totalWidth) * 100;
    
    const Slider = () => {
      const rootRef = useRef<HTMLDivElement>(null);
    
      const handleContainerMouseUp = () => {
        if (rootRef.current) rootRef.current.removeAttribute("data-dragging");
      };
      const handleThumbMouseDown = () => {
        if (rootRef.current) rootRef.current.setAttribute("data-dragging", "true");
      };
    
    // <------- 5
      const updateSliderFillByEvent = (e: React.MouseEvent<HTMLDivElement>) => {
        const elem = rootRef.current;
        if (elem) {
          const rect = elem.getBoundingClientRect();
    
          const fillWidth = computeCurrentWidthFromPointerPos(
            e.pageX,
            rect.left,
            680
          );
          if (fillWidth < 0 || fillWidth > 100) {
            return;
          }
    
          rootRef.current?.style.setProperty("--slider-fill", `${fillWidth}%`);
          return fillWidth;
        }
      };
    
    // <------- 6
      const handleContainerMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
        if (rootRef.current?.getAttribute("data-dragging")) {
          updateSliderFillByEvent(e);
        }
      };
    
      return (
        <StyledContainer
          className="slider"
          onMouseUp={handleContainerMouseUp}
          onMouseMove={handleContainerMouseMove} // <------- 7
          ref={rootRef}
        >
          <StyledTrack className="slider-track"></StyledTrack>
          <StyledSliderFill className="slider-fill"></StyledSliderFill>
          <StyledThumb
            className="slider-thumb"
            onMouseDown={handleThumbMouseDown}
          ></StyledThumb>
        </StyledContainer>
      );
    };
    
    export default Slider;
    ```
    
    This change is a bit tricky so let us go through it step-by-step. Follow the numbering of the comments mentioned in the above code:

- In the `StyledContainer` component, we initialize the custom CSS variable: `--slider-fill` with the value of 0%. We initialize it in the container so that this value gets available to all the children elements.
    
    ```tsx
    const StyledContainer = styled.div`
      --slider-fill: 0%; // <--------- 1
     ...
    `; 
    ```
- Next, we make use of this variable inside the `StyledSliderFill` component with the help of var CSS function. The second argument is nothing but a default value. `--slider-fill` variable is used here because we want to increase the width of the slider-fill component whenever the `--slider-fill` changes. 
```tsx
const StyledSliderFill = styled.div`
  ...
  width: var(--slider-fill, 0%); // <--------- 2
  ...
`;
```
- We also make use of this variable inside the `StyledThumb` component. We use it here because we want to move the `slider-thumb` whenever the variable changes. 
```tsx
const StyledThumb = styled.div`
  ...
  ...
  left: var(--slider-fill, 0%); // <--------- 3
  ...
`;
```
- Next, we create a utility function: `computeCurrentWidthFromPointerPos` the purpose of this function is to compute the current width from the current mouse pointer position. We will see its usage in the `mousemove` event:
```tsx
 const computeCurrentWidthFromPointerPos = (
  xDistance: number,
  left: number,
  totalWidth: number
) => ((xDistance - left) / totalWidth) * 100;
```
The function returns the width in percentage. It takes the following parameters:

- `xDistance` - [clientX](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/clientX) or [pageX](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/pageX) of the current mouse pointer event.
- `left` - Distance of the left edge from the viewport i.e. [getClientBoundingRect()](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect).left
- `totalWidth` - total width of the slider track

- Next, we create a new function: `updateSliderFillByEvent`. The purpose of this function is to set the CSS variable’s value.
```tsx
const updateSliderFillByEvent = (
    variableName: SliderCSSVariableTypes,
    e: React.MouseEvent<HTMLDivElement>
  ) => {
    const elem = rootRef.current;
    if (elem) {
      const rect = elem.getBoundingClientRect();

      const fillWidth = computeCurrentWidthFromPointerPos(
        e.pageX,
        rect.left,
        680
      );
      if (fillWidth < 0 || fillWidth > 100) { // limit
        return;
      }

      rootRef.current?.style.setProperty(variableName, `${fillWidth}%`);
    }
  };
```
A thing to note here is that we need to limit the execution of this function when the `mousemove` event occurs. That means if the mouse pointer moves outside the bounds of the slider track i.e. when it goes below 0 or above 100 then we don’t need to set the CSS variable and exit out of the function which we have done in the above code annotated with the comment limit.

- Next, we create the event handler for `mousemove` event: `handleContainerMouseMove`. Inside this, we call the `updateSliderFillByEvent` function only when `data-dragging` attribute is present in the `StyledContainer`:
    
    ```tsx
    const handleContainerMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
        if (rootRef.current?.getAttribute("data-dragging")) {
          updateSliderFillByEvent("--slider-fill", e);
        }
      };
    ```

- Lastly, we bind this function to the `StyledContainer` with the `onMouseMove` event. 

Our results will look like below:

![slider-dragging](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ia8grmc0k9fok24o4jo9.gif)

### Make slider-track clickable

To make slider-track clickable all we need to do is to bind an event handler to the `onClick` event of the `StyledTrack` component. The event handler should make sure that it calls the `updateSliderFillByEvent` function.

```tsx
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
    if (rootRef.current) {
      updateSliderFillByEvent("--slider-fill", e);
    }
  };

...
...
...

return <StyledContainer ...>
					<StyledTrack onClick={handleClick}/>
						...
						...
				</StyledContainer>
```

This is how the slider track becomes clickable.

![slider clickable](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/azzk2ven74r2cvdcc8lj.gif)

### Styling and refactoring the slider component
Now that our basic functionality is implemented, now let us start adding some sugar coating to this component with styling. Here are some styles and animation that are expected from this component:

- The slider thumb should only be visible when it is hovered

![knob visibility](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3uqsc52dy3beqiqjq3ui.gif)

- While dragging the slider thumb, the thumb should have a diffused background ring of the same color.

![slider-knob-diffused-bg](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h52vnv14cn9r5uab5t4k.gif)

Let's begin with these styling:
- **Make slider-thumb visible only on hover:**
    - Here it is important to understand that the thumb will only be visible when we hover on the entire slider. So whenever the slider is hovered we make the thumb visible.
    - To achieve this we need to toggle the opacity of the slider-thumb.
    - Initially, the opacity will be 0 but when it is hovered we will turn its opacity to 1.
    - Let us add initial styles to the `StyledThumb` component:
    ```tsx
    const StyledThumb = styled.div`
  height: 15px;
  width: 15px;
  border-radius: 50%;
  background-color: red;
  position: absolute;
  bottom: 35%;
  left: 25%;
  transform: translate(-50%, 15%);
  
	z-index: 1;
	opacity: 0;
	transition: opacity 0.2s ease;
`;
    ```
    Here we added the transition property that tells that whenever the opacity changes, do it with the easing function with an animation duration of 0.2s.
    - Inside the `StyledContainer`, we add the CSS which makes sure that the opacity is 1 whenever we hover over this container:
    
    ```tsx
    const StyledContainer = styled.div`
      --slider-fill: 0%;
      position: relative;
      height: 30px;
      display: flex;
      flex-direction: column;
      justify-content: center;
      width: 680px;
    
      // Show the thumb on hover
      &:hover {
        & .slider-thumb {
          opacity: 1;
        }
      }
    `;
    ```
    
    - Our result will be something like below:

    ![final-slider-knob-visibility](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ry9jvhdwy8h9lgp6466d.gif)

- **Add a diffused background ring when the slider-thumb is getting dragged:**
    - The ring that we are talking about here is something that looks like below:
![bg-exp](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mwz0qljppiajeysm4fqe.png)
    - To achieve this, we make use of the before CSS psuedo element. We need to style this element a bit differently. It's going to be the same as the slider-thumb but slightly bigger in radius.
    - Add the below CSS to `StyledThumb`:
    
    ```tsx
    const StyledThumb = styled.div`
      height: 15px;
      width: 15px;
      border-radius: 50%;
      background-color: red;
      position: absolute;
      bottom: 35%;
      left: var(--slider-fill, 0%);
      transform: translate(-50%, 15%);
      z-index: 1;
      opacity: 0;
      transition: opacity 0.2s ease, box-shadow 0.2s ease;
    
      // slider thumb background ring
      &::before {
        content: " ";
        display: inline-block;
        background-color: red;
        height: 24px;
        width: 24px;
        border-radius: 50%;
        opacity: 0;
        transition: opacity 0.2s ease;
        filter: opacity(0.5);
        transform: translate(-18%, -18%);
      }
    `;
    ```
    
    - Now if you try to drag the thumb you still won’t see the background ring because the `::before` element’s opacity is 0 initially and would get set to 1 when the actual dragging happens.
    - To enable this ring only while dragging, we make use of the `data-dragging` attribute that gets enabled/added to the `StyledContainer` whenever the `mousedown` event is happening. We add the following CSS to achieve the same to the `StyledContainer` component:

```tsx
const StyledContainer = styled.div`
  --slider-fill: 0%;
  position: relative;
  height: 30px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  width: 680px;

  // For animating ring behind the thumb; <-----------
  &[data-dragging] {
    & .slider-thumb::before {
      opacity: 1;
    }
  }

  // Show the thumb on hover
  &:hover {
    & .slider-thumb {
      opacity: 1;
    }
  }
`;
```
- This helps us to achieve the following thing:
    
![bg-diffused](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dk3nmhopq00wdgrrio3g.gif)

### Cleaning up the code

* If you have observed carefully we are making use of static width of 680px in the above Slider component i.e the width of 680px in the StyledContainer. This should be dynamic so that the slider can be of any size. Let’s make sure that the component accepts a prop that determines the total width of the slider.
  * Let us create an interface that mandates a prop named $total
    ```tsx
interface SliderProps {
  $total: number;
}
    ```
  * Now enable the `StyledContainer` to accept the `$total` prop by passing the interface to the component:
    
    ```tsx
    const StyledContainer = styled.div<SliderProps>`
      --slider-fill: 0%;
      position: relative;
      height: 30px;
      display: flex;
      flex-direction: column;
      justify-content: center;
      width: ${(props) => props.$total}px;  <-----------
    
      // For animating ring behind the thumb;
      &[data-dragging] {
        & .slider-thumb::before {
          opacity: 1;
        }
      }
    
      // Show the thumb on hover
      &:hover {
        & .slider-thumb {
          opacity: 1;
        }
      }
    `;
    ```
  * We also make sure that we extract this `total` prop from the main Slider component like below:
    
    ```tsx
    const Slider = (props: SliderProps) => {
      const { $total } = props; // <----------------
      
    		...
    		...
    		...
    		...
    	
      const updateSliderFillByEvent = (
        variableName: SliderCSSVariableTypes,
        e: React.MouseEvent<HTMLDivElement>
      ) => {
        const elem = rootRef.current;
        if (elem) {
          const rect = elem.getBoundingClientRect();
    
          const fillWidth = computeCurrentWidthFromPointerPos(
            e.pageX,
            rect.left,
            $total // <----------------
          );
          if (fillWidth < 0 || fillWidth > 100) {
            return;
          }
    
          rootRef.current?.style.setProperty(variableName, `${fillWidth}%`);
        }
      };
    		...
    		...
    		...
    		...
    
      return (
        <StyledContainer
          className="slider"
          onMouseUp={handleContainerMouseUp}
          onMouseMove={handleContainerMouseMove}
          ref={rootRef}
          $total={$total} // <----------------
        >
    	    ...
    			...
    			...
    			...
        </StyledContainer>
      );
    };
    ```
  * We make sure that we accept the total prop from the slider component and also pass it on to the `StyledContainer` to set its width. We also update the same in the `updateSliderFillByEvent` so that `computeCurrentWidthFromPointerPos` takes in the `$total` value.
  * Now we have completed the change let us test it out. Render a couple of slider components in our Main app with different widths and observe the change:
   ```tsx
const App = () => {
  return (
    <>
      <Slider total={300} />
      <Slider total={400} />
      <Slider total={500} />
    </>
  );
};
   ```
* Let us also add a new prop named `$fillColor`. The purpose of this prop is to make sure that the Slider component can have a different set of fill colors. This fill color will be applied to the slider-fill and the slider-thumb.
  * Add prop in the `SliderProps` interface:
    
    ```tsx
    interface SliderProps {
      $total: number;
    	$fillColor?: string;
    }
    ```
  * Accept the same prop in `Slider` component and pass it on to `StyledContainer`:
    ```tsx
const StyledContainer = styled.div<SliderProps>`
  --slider-fill: 0%;
  --slider-fill-color: ${(props) => props.$fillColor}; // <-----------
  position: relative;
  height: 30px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  width: ${(props) => props.$total}px;
  &[data-dragging] {
    & .slider-thumb::before {
      opacity: 1;
    }
  }
  &:hover {
    & .slider-thumb {
      opacity: 1;
    }
  }
`;
const Slider = (props: SliderProps) => {
  const { $total, $fillColor='white' } = props; // <---
		...
		...
		...
  return (
    <StyledContainer
      className="slider"
			$fillColor={$fillColor}
      onMouseUp={handleContainerMouseUp}
      onMouseMove={handleContainerMouseMove}
      ref={rootRef}
      $total={$total}
    >
			...
			...
			...
    </StyledContainer>
  );
};
```
  * We set the `$fillColor` to the custom CSS variable: `--slider-fill-color`, so that it can be used by `StyledSliderFill` and `StyledThumb` components like below:
   ```tsx
const StyledSliderFill = styled.div`
  height: 5px;
  background-color: var(--slider-fill-color); // <---------
  width: var(--slider-fill, 0%);
  position: absolute;
  pointer-events: none;
`;

const StyledThumb = styled.div`
	...
	...
  background-color: var(--slider-fill-color); // <---------
	...
	...

  // slider thumb background ring
  &::before {
		...
		...
    background-color: var(--slider-fill-color); // <---------
		...
		...
		...
  }
`;
   ```
  * This yields the following results:
   ```tsx
const App = () => {
  return (
    <>
      <Slider total={300} $fillColor="red" />
      <Slider total={400} $fillColor="green" />
      <Slider total={500} $fillColor="blue" />
    </>
  );
};
   ```
  * Our final Slider component will look like below:
```tsx
import { useRef } from "react";
import styled from "styled-components";

interface SliderProps {
  total: number;
  $fillColor?: string;
}

const StyledContainer = styled.div<SliderProps>`
  --slider-fill: 0%;
  --slider-fill-color: ${(props) => props.$fillColor};
  position: relative;
  height: 30px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  width: ${(props) => props.total}px;

  // For animating ring behind the thumb;
  &[data-dragging] {
    & .slider-thumb::before {
      opacity: 1;
    }
  }

  // Show the thumb on hover
  &:hover {
    & .slider-thumb {
      opacity: 1;
    }
  }
`;

const StyledTrack = styled.div`
  width: 100%;
  height: 5px;
  background-color: #cacaca;
  position: absolute;
  pointer-events: auto;
`;

const StyledSliderFill = styled.div`
  height: 5px;
  background-color: var(--slider-fill-color);
  width: var(--slider-fill, 0%);
  position: absolute;
  pointer-events: none;
`;

const StyledThumb = styled.div`
  height: 15px;
  width: 15px;
  border-radius: 50%;
  background-color: var(--slider-fill-color);
  position: absolute;
  bottom: 35%;
  left: var(--slider-fill, 0%);
  transform: translate(-50%, 15%);
  z-index: 1;
  opacity: 0;
  transition: opacity 0.2s ease, box-shadow 0.2s ease;

  // slider thumb background ring
  &::before {
    content: " ";
    display: inline-block;
    background-color: var(--slider-fill-color);
    height: 24px;
    width: 24px;
    border-radius: 50%;
    opacity: 0;
    transition: opacity 0.2s ease;
    filter: opacity(0.5);
    transform: translate(-18%, -18%);
  }
`;

const computeCurrentWidthFromPointerPos = (
  xDistance: number,
  left: number,
  totalWidth: number
) => ((xDistance - left) / totalWidth) * 100;

export type SliderCSSVariableTypes = "--slider-fill" | "--slider-pointer";

const Slider = (props: SliderProps) => {
  const { total, $fillColor = "white" } = props;
  const rootRef = useRef<HTMLDivElement>(null);

  const handleContainerMouseUp = () => {
    if (rootRef.current) rootRef.current.removeAttribute("data-dragging");
  };
  const handleThumbMouseDown = () => {
    if (rootRef.current) rootRef.current.setAttribute("data-dragging", "true");
  };

  const updateSliderFillByEvent = (
    variableName: SliderCSSVariableTypes,
    e: React.MouseEvent<HTMLDivElement>
  ) => {
    const elem = rootRef.current;
    if (elem) {
      const rect = elem.getBoundingClientRect();

      const fillWidth = computeCurrentWidthFromPointerPos(
        e.pageX,
        rect.left,
        total
      );
      if (fillWidth < 0 || fillWidth > 100) {
        return;
      }

      rootRef.current?.style.setProperty(variableName, `${fillWidth}%`);
    }
  };

  const handleContainerMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
    if (rootRef.current?.getAttribute("data-dragging")) {
      updateSliderFillByEvent("--slider-fill", e);
    }
  };

  const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
    if (rootRef.current) {
      updateSliderFillByEvent("--slider-fill", e);
    }
  };

  return (
    <StyledContainer
      className="slider"
      $fillColor={$fillColor}
      onMouseUp={handleContainerMouseUp}
      onMouseMove={handleContainerMouseMove}
      ref={rootRef}
      total={total}
    >
      <StyledTrack className="slider-track" onClick={handleClick}></StyledTrack>
      <StyledSliderFill className="slider-fill"></StyledSliderFill>
      <StyledThumb
        className="slider-thumb"
        onMouseDown={handleThumbMouseDown}
      ></StyledThumb>
    </StyledContainer>
  );
};

export default Slider;
```

![multi-color-slider](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bmeeufsbwlkhq1oy6tky.png)

## Building the Volume Slider component
n this section, we will be implementing the volume slider component that we talked about earlier. The basic part of this component is the Slider which we have implemented above. The hard part is done. We are just left with using it to control the volume of the video.

To create a volume slider we need the following things:
* We need a new state in our global state: volume that will help us to keep track of the current volume that is changed by our slider. If you have read the previous posts, then you would also know that when this state changes we will also update the volume property of the video element. 
Let’s add a new state:
  * Update the Global State interface and the global state in the `context/index.tsx` file:
    
    ```tsx
    export type StateProps = {
    		...
        volume: number;
    		...
    };
    
    export const initialState: StateProps = {
    		...
        volume: 1,
        ...
    };
    
    ```
  * Next, we create action: `VOLUME_CHANGE` so that it can be dispatched. Add the following line at the end in the `context/actions.ts` file:
    ```tsx
export const VOLUME_CHANGE = 'VOLUME_CHANGE';
    ```
  * Next, we also need to update the reducer function. Add the below case in the reducer function present in the `context/reducer.ts` file:
    
    ```tsx
    case VOLUME_CHANGE: {
                return {
                    ...state,
                    volume: action.payload,
                };
            }
    ```
  * Finally, we add an `useEffect` in the `src/components/Video.tsx` that updates the actual video’s volume when the above state’s volume changes:
    ```tsx
useEffect(() => {
        if (videoRef.current) {
            videoRef.current.volume = volume;
        }
    }, [volume]);
    ```
* Now it is time for us to create the Volume Slider component. Create a file named: VolumeSlider.tsx inside the src/components/VolumeControl folder and paste the following code:
```tsx
import { useContext, useEffect, useRef } from 'react';
import styled from 'styled-components';
import { PlayerDispatchContext } from '../../context';
import { VOLUME_CHANGE } from '../../context/actions';
import Slider, { SliderRefProps } from '../common/Slider';
import Tooltip from '../common/Tooltip';

const StyledContainer = styled.div`
    width: 0px;
    transition: width 0.2s ease;
`;

const VolumeSlider = () => {
    const sliderRef = useRef<SliderRefProps>(null);
    const dispatch = useContext(PlayerDispatchContext);

    const onPositionChangeByDrag = (currentPercentage: number) => {
        let newVolume = currentPercentage / 100;
        if (newVolume <= 0.03) {
            newVolume = 0;
        }
        if (newVolume > 1) {
            newVolume = 1;
        }
        dispatch({
            type: VOLUME_CHANGE,
            payload: newVolume,
        });
    };

    const onPositionChangeByClick = (currentPercentage: number) => {
        const newVolume = currentPercentage / 100;

        dispatch({
            type: VOLUME_CHANGE,
            payload: newVolume,
        });
    };

    useEffect(() => {
        if (sliderRef.current) {
            sliderRef.current.updateSliderFill(100);
        }
    }, []);

    return (
        <Tooltip content="Volume">
            <StyledContainer className="control--volume-slider">
                <Slider total={60} onClick={onPositionChangeByClick} onDrag={onPositionChangeByDrag} ref={sliderRef} />
            </StyledContainer>
        </Tooltip>
    );
};

export default VolumeSlider;
```
A couple of things happening here Let go from top to bottom:
  * We use `StyledContainer` which is a styled component with a `width` of `0px` and transition property being set on the width of this component. We do this for animation purposes, which we will look at in the later section.
  * Next, when the volume slider is loaded/mounted we want the slider to represent the full volume i.e. slider-fill’s width should be 100%. To do that we need a way to update the slider component’s `--slider-fill` CSS custom variable from the Volume Slider component.
  * To achieve this, we need to expose a function from the Slider component that updates the `--slider-fill` CSS variable. This exposed function will be accessible to the `ref` that is passed on to the Slider component from the Volume Control component. This can be done by adding a react’s `useImperativeHandle` hook. You can read more about it [here](https://react.dev/reference/react/useImperativeHandle).
  * Make the following changes pointed out in the comments below in the Slider component:
    ```tsx
import { forwardRef, Ref, useImperativeHandle, useRef } from "react";
import styled from "styled-components";
...
...
...
export interface SliderRefProps { // <----------
  updateSliderFill: (completedPercentage: number) => void;
}
const Slider = (props: SliderProps, ref: Ref<SliderRefProps>) => {
  ...
  ...
  useImperativeHandle( // <----------
    ref,
    () => {
      return {
        updateSliderFill(percentageCompleted: number) {
          rootRef.current?.style.setProperty(
            "--slider-fill",
            `${percentageCompleted}%`
          );
        },
      };
    },
    []
  );
  return (
    <StyledContainer
      className="slider"
      $fillColor={$fillColor}
      onMouseUp={handleContainerMouseUp}
      onMouseMove={handleContainerMouseMove}
      ref={rootRef}
      total={total}
    >
      <StyledTrack className="slider-track" onClick={handleClick}></StyledTrack>
      <StyledSliderFill className="slider-fill"></StyledSliderFill>
      <StyledThumb
        className="slider-thumb"
        onMouseDown={handleThumbMouseDown}
      ></StyledThumb>
    </StyledContainer>
  );
};
export default forwardRef(Slider);
    ```
  * Now we have exposed the `updateSliderFill` function, so to achieve what we wanted i.e. to have 100% volume on mount, there is a `useEffect` hook that is added in the VolumeSlider component. It checks if the sliderRef exists, and then sets the `--slider-fill` to 100 with the help of `updateSliderFill` function.
  * Next, we look at how the volume changes when the thumb is dragged or when the slider track is clicked. We have two functions namely: `onPositionChangeByDrag` and `onPositionChangeByClick`. All these functions do is compute the current volume from the `completedPercentage` and update the global store with the `dispatch` action of `VOLUME_CHANGE`.
 
  * Later in the return section as well, we pass these functions as `onDrag` and `onClick`. But even if these functions are passed as a prop still they won’t execute because we are not calling these inside the `Slider` component. 
    
  * To do this, we should update our `handleContainerMouseMove` and `handleClick` functions in the `Slider` component as follows:
    ```tsx
// Update the Slider prop interface as well like below:
interface SliderProps
  extends Omit<React.HTMLAttributes<HTMLDivElement>, "onClick" | "onDrag"> {
  total: number;
  $fillColor?: string;
  onClick?: (currentPercentage: number) => void;
  onDrag?: (completedPercentage: number) => void;
}
const Slider = (props: SliderProps, ref: Ref<SliderRefProps>) => {
const { total, onDrag, onClick, $fillColor = "white" } = props;
...
...
...
const handleContainerMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
    if (rootRef.current?.getAttribute("data-dragging")) {
      updateSliderFillByEvent("--slider-fill", e);
      const fillValue = rootRef.current.style.getPropertyValue("--slider-fill");
      const width = Number(fillValue.split("%")[0]);
      onDrag?.(width); // Call the functions passed from volume slider
    }
  };
  const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
    if (rootRef.current) {
      updateSliderFillByEvent("--slider-fill", e);
      const fillValue = rootRef.current.style.getPropertyValue("--slider-fill");
      const width = Number(fillValue.split("%")[0]);
      onClick?.(width); // Call the functions passed from volume slider
    }
  };
	return <StyledContainer
      className="slider"
      $fillColor={$fillColor}
      onMouseUp={handleContainerMouseUp}
      onMouseMove={handleContainerMouseMove}
      ref={rootRef}
      total={total}
    >
      <StyledTrack className="slider-track" onClick={handleClick}></StyledTrack>
      <StyledSliderFill className="slider-fill"></StyledSliderFill>
      <StyledThumb
        className="slider-thumb"
        onMouseDown={handleThumbMouseDown}
      ></StyledThumb>
    </StyledContainer>
}
    ```

## Animating the opening of the Volume Slider
The last piece for the Volume Slider component is to only make it visible when we hover over the entire volume control component. To make this happen we should make the following change in the `StyledVolumeControl` of the `VolumeControl` component present inside the `src/components/VolumeControl/index.tsx`:

```tsx
import styled from 'styled-components';
import MuteButton from './MuteButton';
import VolumeSlider from './VolumeSlider';

const StyledVolumeControl = styled.div`
    width: 20%;
    display: flex;

    &:hover { // <----- The change
        & .control--volume-slider {
            width: 60px;
        }
    }
`;

const VolumeControl = () => {
    return (
        <StyledVolumeControl className="volume-control">
            <MuteButton />
            <VolumeSlider />
        </StyledVolumeControl>
    );
};

export default VolumeControl;
```

Remember in the previous section we talked about having width to be 0 and transition property being set to width change. That will help us here because initially the width will be 0 and when the `VolumeControl` is hovered we will make sure that `VolumeSlider` width is set to `60px`. 

Here is what it will look like:

![volume-slider-opening](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/19dhgb4hcthn0n2iz6m1.gif)

## Summary

To summarize we learned the following things in this post:

- We learned the structure of the Volume Control
- We saw how Mute/Unmute button is implemented
- We implemented the slider component from scratch.
- We integrated the slider with the volume control
- Finally, we saw how we animated the volume slider opening

In the next blog post of this series, we are going to talk about building another crucial control which is Seekbar so stay tuned for more!!

The entire code for this tutorial can be found [here](https://github.com/keyurparalkar/react-youtube-player-clone).

Thank you for reading!

Follow me on [twitter](https://twitter.com/keurplkar), [github](http://github.com/keyurparalkar), and [linkedIn](https://www.linkedin.com/in/keyur-paralkar-494415107/).