On Today’s mobile world, sometimes is preferable to scroll sections horizontally rather than vertically, being the most common example a gallery of images. There are tones of libraries that implement such solution on various fancy ways but they are all javascript based and most them have a heavy footprint and may require tones of others libraries.
If you need a light and native solution, the problem can be also solved with CSS by using the calc function. The calc function is not as powerful as writing javascript code but it is supported since some years by all modern browsers.
Below we illustrate the usage of the calc function to ensure that a DIV element has always only the horizontal scrollbar while displaying a collection of child elements. It assumes that the number of children as well as its size is known.
If you don’t have time for explanations and wants to go to the solution right away, a working example can be found below:
https://jsfiddle.net/bmarotta/7c7n832o/
The layout
To achieve our goal we need:
- One DIV (or any other container) that will frame the space to be used
- One DIV whose width will grow horizontally depending on the contents size and container height
- The content. In our case a bunch of DIVs but it could as well have been images or labels, as long as we could predict its dimension.
For our example we called the first DIV the outer, the second DIV inner. We assume that each child DIV has 150px width and 100px height with 10px margin on all sides.
The Formula
As mentioned in the header, to adjust the width we use the CSS calc function. Our formula should be:
<# of items> / (<total height> / <item height>) * <item width>
Explanation
- Find out how many rows fit in the container: available height divided by the individual item height.
- Find out how many items we will have per row by dividing the number of items by the result of point 1.
- Multiply the number of items per row by the item width (considering the margin).
To get the total height we use the value ‘100vh‘ meaning 100% of the viewport height. If we assume 20 items our CSS should look something like this:
.inner { width: calc(20 / (100vh / 120px) * 180px) }
This would be too simple if we didn’t have many setbacks:
- The calc function works with float numbers. To get it right we would have to work with integer numbers or something like a ceil or floor functions
- The calc function is very restrictive in terms of units that can be used, order of operators and others things. Setting it right for complex formulas is a tedious trial-and-error work
- If you put space or blank lines between the child DIV items some additional pixels will be added between each one of them and the calculation might be wrong
To overcome the first problem we simply simulate one item more for each row, or:
.inner { width: calc(20 / ((100vh / 120px) + 1) * 180px) }
The second problem is a little bit trickier. It requires several iterations on the browser style editor and some arithmetic simplifications. During the implementation I found out that the calc function doesn’t always allow me to add the px unit. For whatever reason I had to remove the last px and multiply the result by 100.
The last point is the easiest one: just remove the blank lines and empty spaces.
The end result is:
.inner { width: calc((20 * 120px / (100vh - 120px) * 100) * 180); }
Last considerations
To workaround the lack of rounding in the calc function we had to add always one item more to each row. Although this prevents the vertical scroll to appears (which was our primary goal) it will adjust the width linearly and not in chunks. For instance, with 1 row one would expect to have 20 items per row, 2 rows 10 items and 3 rows 7 items plus 6 items in the last row. The effect from the above formula without rounding is that with 2 rows we might get 10, 11, 12 to 19 items in the first row depending on the viewport height.
To minimize this effect we can add media queries for the 3 first cases (1, 2 and 3 rows) where this effect is more visible:
@media all and (max-height: 490px) { .inner { width: 1260px; /* for browsers that don't implement calc */ width: calc(7 * 180px); } }
@media all and (max-height: 370px) { .inner { width: 1800px; /* for browsers that don't implement calc */ width: calc(10 * 180px); } }
@media all and (max-height: 250px) { .inner { width: 3600px; /* for browsers that don't implement calc */ width: calc(20 * 180px); } }