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

Correcting the invalid syntax due to EOF issue

How can we resolve the end of file error? The brackets appear to be valid based on ecma standards, but it's not clear what is missing. After using jsonlint, this error was found: *Error: Parse error on line 16: ...States" }] }]}{ "i ...

Display a sneak peek on a separate tab

I have an editor on my website where users can input and edit their own HTML code. Instead of saving this to a database, I want to display the current user's HTML code in a new window using JavaScript. How can I achieve this without storing the code p ...

Unable to change background-image as intended

Here is an example of my HTML code with an initial background image: <div id="ffff" style="width: 200px; height: 200px; background-image: url('/uploads/backroundDefault.jpg')">sddsadsdsa<br>dffdsdfs</div> Everything seems to b ...

Develop a search feature that automatically filters out special characters when searching through a

I am currently developing a Vue-Vuetify application with a PHP backend. I have a list of contacts that include first names, last names, and other details that are not relevant at the moment. My main query is how to search through this list while disregardi ...

Switching CodeIgniter's controller to the primary controller within a designated div

<li><a href="maine/home#services">SERVICES</a></li> <li><a href="maine/home#about">ABOUT</a></li> <li><a href="maine/home#contact">CONTACT</a></li> I'm currently using CodeIgnite ...

sanitize-html does not support the <br> tag when using react-quill

Within my react app, I utilize react-quill for a text editor and sanitize-html with JOI for validation on the backend. Interestingly, when leaving empty lines in the text editor, it generates "<p><br></p>". By default, sanitize-html does ...

Using app.js in a blade file can cause jQuery functions and libraries to malfunction

In my Laravel application, I am facing an issue with my vue.js component of Pusher notification system and the installation of tinymce for blog posts. Adding js/app.js in my main layout blade file causes my tinymce and other jQuery functions to stop workin ...

Trying out a React component that relies on parameters for connection

Having an issue while attempting to test a connected react component that requires a props.params.id in order to call action creators. During the testing process, when checking if the component is connected to the store, an error "Uncaught TypeError: Canno ...

Error: $this.text is throwing a TypeError and is not working as a function

Upon examining the code below: var $this = $(this).children('div.submenu1').children('a.subtile')[0], title = $this.text(), name = $this.attr('node').val; An error is encountered: Uncaught TypeError: $this.text is not a fun ...

Highlight the parent name in the menu when I am on the corresponding child page

Here is a snippet of a recursive function: function recursive($arrays, $out) { if (is_array($arrays)){ //$out .= "<ul>"; foreach($arrays as $parent => $data) { //if parent is empty if ($parent === '') { ...

How to retrieve values from HTML class names using Javascript for loops but encountering issues

foreach($products as $row){ <input type="hidden" class="prodId" name="id" value="<?php echo $row['id']; ?>"> <input type="hidden" class="prodUnique" name="unique" value="<?php echo $unique; ?>"> <button id="added" ...

Could you specify the type of useFormik used in formik forms?

For my react formik form, I have created multiple components and now I am looking for the right way to pass down the useFormik object to these components. What should be the correct type for formik? Main Form const formik = useFormik({ ... Subcomponent ...

Ways to combine X and Y velocities into a single velocity

Is there a way to combine the X and Y Velocity into a single Velocity without considering the angle? var velocityX = some value; var velocityY = some value; // Need to convert both X and Y velocities into one combined velocity ...

Combining two arrays filled with objects to form a collection of Objects on a Map

In my JavaScript code, I'm receiving data from a WebService that looks like this: { "fire": { "totalOccurence": 2, "statsByCustomer": [ { "idCustomer": 1, "occurence": 1 }, { "idCustomer": 2, ...

What is the best way to alternate $httpBackend when[method] declarations in unit tests to handle multiple requests?

When conducting my testing, I set up the model data and mock the response: beforeEach(function(){ var re = new RegExp(/^http\:\/\/.+?\/users-online\/(.+)$/); $httpBackend.whenGET(re).respond({id:12345, usersOnline:5000}); }) ...

What is the reason for including parentheses when evaluating JSON data?

What is the purpose of adding ( and ) around the code when using eval? var strJson = eval("(" + $("#status").val().replace(";","") + ")"); Note: The result of $("#status").val() is similar to {"10000048":"1","25000175":"2","25000268":"3"}; ...

Developing a progress bar with jQuery and Cascading Style Sheets (

Below is the code I'm currently using: <progress id="amount" value="0" max="100"></progress> Here is the JavaScript snippet I have implemented: <script> for (var i = 0; i < 240; i++) { setTimeout(function () { // this repre ...

No pathways can be established within Core UI Angular

I've been attempting to use the router link attribute to redirect to a new page, but instead of landing on the expected page, I keep getting redirected to the dashboard. Below is an overview of how my project's structure looks: [![enter image de ...

Displaying the second image twice in a jQuery image slider

When attempting to implement a slider on my website, I encountered an issue where the second image appears twice in a row before the slider starts working as expected. I'm struggling to figure out why this is happening. Here is the jQuery code I am u ...

Angular CSS: ng-style applied to only one side, the other remains unaffected

I am working on an Angular application that requires a split screen with two halves, each displaying the same array values. The width and height values are set using a service and accessed by a controller. The style is applied using ng-style in the DOM. Ho ...