JS custom scrollbar thumb size issues in relation to the scroll width of the element

I recently implemented a custom scrollbar for a component on my website.

To determine the length of the scrollbar thumb, I use the formula

viewportWidth / element.scrollWidth;
. This provides me with a percentage value that I then apply to the thumb element.

However, I encountered an issue where once the viewportWidth reaches a specific value (622px in my case), the scrollbar breaks. The content consists of boxes, each 350px wide with 16px margins on either side, resulting in each box taking approximately 382 pixels. When this happens, the scrollbar becomes longer than it should be, requiring the thumb to be moved outside the scrollbar range to scroll all the way left.

Below is the relevant code snippet:

...

Despite thorough checks, I cannot pinpoint exactly why the issue occurs. It seems that all the variables involved - scrollWidth, viewportWidth, xLocation, and getFullWidth() - provide accurate values. This leaves me puzzled as to the root cause of the problem.

If anyone has insights or suggestions on how to resolve this confusion, your input would be greatly appreciated.

Answer №1

const restrictRange = (value, min, max) => {
    return Math.min(Math.max(value, min), max);
  }

class ScrollableFrame extends React.Component {
  state = { isHolding: false, positionX: 0, frameWidth: 0 };
  lastPosition = 0;

  constructor(props) {
    super(props);
    this.trackRef = React.createRef();
    this.contentRef = React.createRef();
  }

  scrollCorrection = 0;

  scrollFrame = amount => {
    this.setState({
      positionX: restrictRange(
        this.state.positionX + amount,
        0,
        this.getTotalWidth() - this.getThumbWidthAbsolute() - this.scrollCorrection
      )
    });
  };

  componentDidMount = () => {
    document.body.addEventListener("mousemove", e => {
      if (this.state.isHolding) {
        let delta = e.pageX - this.lastPosition;
        this.scrollFrame(delta);
        this.lastPosition = e.pageX;
      }
    });
    document.body.addEventListener("mouseup", e => {
      this.setState({ isHolding: false });
    });
  };

  getTotalWidth = () => {
    return this.trackRef.current
      ? this.trackRef.current.clientWidth
      : this.defaultSize;
  };
  contentScrollWidth = () => {
    if (this.contentRef.current) {
      return this.contentRef.current.scrollWidth;
    }
  };
  contentViewportWidth = () => {
    if (this.contentRef.current) {
      return this.contentRef.current.clientWidth;
    }
  };

  getRelativeThumbWidth = () => {
    // console.log(this.getTotalWidth(), this.contentScrollWidth());
    return this.getTotalWidth() / this.contentScrollWidth();
  };

  getThumbWidthAbsolute = () => {
    return this.getRelativeThumbWidth() * this.getTotalWidth();
  };

  defaultSize = 100;
  render() {
    let calculatedWidth = this.getRelativeThumbWidth();
    let thumbPosition =
      this.state.positionX /
      (this.getTotalWidth() - this.getThumbWidthAbsolute() - this.scrollCorrection);

    console.log(thumbPosition);

    let scrollDistance =
      thumbPosition * (this.contentScrollWidth() - this.contentViewportWidth());

    // console.log(thumbPosition, scrollAmount);

    if (this.contentRef.current) {
      this.contentRef.current.scrollLeft = scrollDistance;
    }
    return (
      <div
        {...this.props}
        onWheel={e => {
          this.scrollFrame(e.deltaY);
        }}
        onTouchMove={e => {
          let newX = e.touches[0].clientX;
          this.scrollFrame(newX - this.lastPosition);
          this.lastPosition = newX;
        }}
      >
        <div ref={this.contentRef} className="overflow-hidden">
          {this.props.children}
        </div>
        <div className={"trackbar" + " mt-auto"} ref={this.trackRef}>
          <span
            style={{
              transform: `translateX(${this.state.positionX}px)`,
              width: calculatedWidth * 100 + "%"
            }}
            onMouseDown={e => {
              this.lastPosition = e.clientX;
              this.setState({ isHolding: true });
            }}
          ></span>
        </div>
      </div>
    );
  }
}

class GalleryItem extends React.Component {
  render() {
    return (
      <div
        className={
          "bg-black mx-4 w-350px h-48 flex-shrink-0 inline-block align-top " +
          "galleryItem"
        }
      ></div>
    );
  }
}

ReactDOM.render(
        <ScrollableFrame
                    className={
                      "transition-all duration-500 ease-in-out my-auto flex flex-col w-full px-8 "
                    }
                  >
                    <div className="inline-block whitespace-no-wrap mb-4 py-8 ">
                      <GalleryItem />
                      <GalleryItem />
                      <GalleryItem />
                      <GalleryItem />  
                      <GalleryItem />                  
                    </div>
                  </ScrollableFrame>,
  document.getElementById("app")
);
.trackbar {
  user-select: none;
  touch-action: none;
  margin-top: 0;
  height: 25px;
  background: black;
  display: flex;
  // padding: 5px;
  overflow: hidden;
}
  span {
    width: 200px;
    cursor: pointer;
    background: #b94747;
  }
  span:hover {
      background: #ff6060;
    }
.galleryItem {
width:350px;
background:black;
  transition: all 0.15s ease-in;
  cursor: pointer;
}
  .previewBox:hover {
    transition: all 0.15s ease-out;
    transform: scale(1.025);
  }
<div id = "app"></div>
<p class="px-8 mt-3">If you make your screen wider, you'll see that scrollbar works OK, if its not wide enough, its thumb will have to go out of viewport to work</p>


<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">

Here lies the issue

span {
    min-width: 200px; // this
    cursor: pointer;
    background: #b94747;
}

To resolve it, change it to width so it can be overridden. You may also need to update the width in the componentDidMount for the initial rendering.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Ajax is capable of sending images as part of the data payload

I am attempting to send data via Ajax, and this data may include an image. Unfortunately, I do not have any forms, so I am unable to submit in the traditional way. Here is my HTML code: <input type="file" accept="image/png,image/jpg,image/jpeg,image/ ...

Guide on how to selectively add middleware such as multer to a fresh express router

For a previous project, I set up a router and utilized multer for handling file uploads. I was able to apply the multer middleware selectively on specific routes as shown below: var router = express.Router(); var multer = require('multer'); var ...

Encountering a type error in Typescript when assigning a transition component to a Material UI Snackbar

Attempting to implement snackbar alert messages using Material UI in a React JS application with TypeScript. Encountering a type error when trying to modify the transition direction of the snackbar. Referenced the snackbar demo from Material UI documentat ...

LeafletJS and ReactJS integration causing misalignment in tile ordering

In my ReactJS single page application, I am utilizing the LeafletJS plugin to showcase a full-page map. Even after following the guidelines mentioned here, I am facing an issue where the map tiles are not displayed in the correct order and appear to be shu ...

Offspring maintain a certain position within the larger framework of their parent on a

When resizing the parent wrap container, how can I ensure that the pin (red dot) on the image maintains its relative position? To see the issue, resize the wrap container. #wrap{ position: absolute; width: 100%; height: 100%; top: 0; l ...

Apply criteria to an array based on multiple attribute conditions

Given an array containing parent-child relationships and their corresponding expenses, the task is to filter the list based on parents that have a mix of positive and negative expenses across their children. Parents with only positive or negative child exp ...

Fix the uneven shape border/outline

I made 3 circles using CSS (:after) and noticed that the border looks uneven with certain background colors. Any suggestions on how to resolve this issue? You can view the problem here: Just below the post title, you'll find the first blue circle bo ...

What methods do publications use to manage HTML5 banner advertisements?

We are working on creating animated ads with 4 distinct frames for online magazines. The magazines have strict size limits - one is 40k and the other is 50k. However, when I made an animated GIF in Photoshop under the size limit, the image quality suffered ...

Displaying various elements with distinct properties in React using ES6

<---------------------MODIFICATION------------------------> I initially thought I needed multiple onChange functions, but after reviewing the answers provided, I discovered a solution thanks to the helpful user's response. With some experimentatio ...

What is a memory-saving method to clear an object in JavaScript?

I am looking for a way to use the same object repeatedly in JavaScript by emptying it after its purpose is served, without creating a new object each time. In arrays, I usually do arr.length=0 to clear an array instead of assigning it to a new memory locat ...

Tips for showing a map in a concealed location: Embed the code in the appropriate location

I've come across solutions for displaying a map in a hidden div, but as a designer and not a programmer, I'm unsure of where to insert the code. The issue is detailed in this post: Click here to view. To resolve the problem, the suggestion is to ...

Assign a class when a path is clicked using Leaflet.js

My goal is to apply a specific class to the clicked polygon by using the following code: function addClass() { function style(feature) { return { className: "active" }; } } function onEachFeature(feature, layer) { ...

I'm currently troubleshooting the code for the Gallery project. The gallery is supposed to have 4x4 grids, but for some reason, the grids are

It seems like I am struggling to identify the exact issue. The display on mobile looks fine but not on desktop. I attempted to tweak the isotope configuration without success. Even manipulating the server-side code didn't reveal any obvious problems. ...

Issue with Material UI Stepper not maintaining state when proceeding forward or backward

I am currently working with the Material UI Stepper Component. My challenge is that when I dynamically render the stepper on the screen using the ADD Button, and then proceed to fill out forms in each step which may include TextFields, SelectBox, etc., if ...

Verify if the radio element is marked as selected in the AJAX reply

My ajax response contains two radio elements and I need to check if they are checked in the response. I've tried using the code below to check the radio status but it's not working: $('#input[type=radio]').each(function(){ alert($( ...

How can we utilize CSS floats to achieve maximum widths?

I currently have 5 divs that I need to structure in a specific way: Each div must have a minimum size of 100px The parent container should display as many divs as possible on the first row, with any remaining divs wrapping to new rows if necessary If the ...

Extracting unique text values from nested div elements within list items using Selenium with Python

Within the HTML code provided, I am looking to extract the text of three variables (descr_confezione, aic_farmaco, and stato_confezione) for each of the four list items: <ul id="ul_lista_confezioni"> <li style="display: list-item;"> &l ...

Increase the date by one day in the minimum date field using the date picker

I've been struggling with adding one day to the minDate in my code. Despite trying multiple solutions from Stackoverflow, none of them have worked successfully. Here is the code I currently have: $('#cal_mulamhn').datepicker({ minDate ...

Leveraging packages obtained from npm repositories

Recently, I came across this guide about React that included a paragraph that left me puzzled. According to the guide, CommonJS modules (found in npm) cannot be directly used in web browsers due to technical limitations. Instead, you need a JavaScript " ...

Using the Mousetrap binding on a nuxt.js page may not be effective

Hey there! I'm trying to achieve a certain functionality where I want to redirect to a different page once a specific sequence is typed. Although I can see the message "It works" in my console, the redirection is not happening and instead, I am gettin ...