How to calculate the amount of flexbox items in a row?

The question is slightly more complex than finding how many items are in a row.

Ultimately, we want to know if there’s an element above, below, left, and right of the active element. And this needs to account for cases where the bottom row is incomplete. For example, in the case below, the active element has no item above, below, or right:

enter image description here

But, in order to determine if there’s an item above/below/left/right of the active item, we need to know how many items are in a row.

Find the number of items per row

To get the number of items per row we need:

  • itemWidth – the outerWidth of a single element including border, padding and margin
  • gridWidth – the innerWidth of the grid, excluding border, padding and margin

To calculate these two values with plain JavaScript we can use:

const itemStyle = singleItem.currentStyle || window.getComputedStyle(active);
const itemWidth = singleItem.offsetWidth + parseFloat(itemStyle.marginLeft) + parseFloat(itemStyle.marginRight);

const gridStyle = grid.currentStyle || window.getComputedStyle(grid);
const gridWidth = grid.clientWidth - (parseFloat(gridStyle.paddingLeft) + parseFloat(gridStyle.paddingRight));

Then we can calculate the number of elements per row using:

const numPerRow = Math.floor(gridWidth / itemWidth)

Note: this will only work for uniform-sized items, and only if the margin is defined in px units.

A Much, Much, Much Simpler Approach

Dealing with all these widths, and paddings, margins, and borders is really confusing. There’s a much, much, much simpler solution.

We only need to find the index of the grid element who’s offsetTop property is greater than the first grid element’s offsetTop.

const grid = Array.from(document.querySelector("#grid").children);
const baseOffset = grid[0].offsetTop;
const breakIndex = grid.findIndex(item => item.offsetTop > baseOffset);
const numPerRow = (breakIndex === -1 ? grid.length : breakIndex);

The ternary at the end accounts for the cases when there’s only a single item in the grid, and/or a single row of items.

const getNumPerRow = (selector) => {
  const grid = Array.from(document.querySelector(selector).children);
  const baseOffset = grid[0].offsetTop;
  const breakIndex = grid.findIndex(item => item.offsetTop > baseOffset);
  return (breakIndex === -1 ? grid.length : breakIndex);
}
.grid {
  display: flex;
  flex-wrap: wrap;
  align-content: flex-start;
  width: 400px;
  background-color: #ddd;
  padding: 10px 0 0 10px;
  margin-top: 5px;
  resize: horizontal;
  overflow: auto;
}

.item {
  width: 50px;
  height: 50px;
  background-color: red;
  margin: 0 10px 10px 0;
}

.active.item {
  outline: 5px solid black;
}
<button onclick="alert(getNumPerRow('#grid'))">Get Num Per Row</button>

<div id="grid" class="grid">
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item active"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
</div>

But is there an item above or below?

To know if there’s an item above or below the active element we need to know 3 parameters:

  • totalItemsInGrid
  • activeIndex
  • numPerRow

For example, in the following structure:

<div id="grid" class="grid">
  <div class="item"></div>
  <div class="item"></div>
  <div class="item active"></div>
  <div class="item"></div>
  <div class="item"></div>
</div>

we have a totalItemsInGrid of 5, the activeIndex has a zero-based index of 2 (it’s the 3rd element in the group), and let’s say the numPerRow is 3.

We can now determine if there’s an item above, below, left, or right of the active item with:

  • isTopRow = activeIndex <= numPerRow - 1
  • isBottomRow = activeIndex >= totalItemsInGid - numPerRow
  • isLeftColumn = activeIndex % numPerRow === 0
  • isRightColumn = activeIndex % numPerRow === numPerRow - 1 || activeIndex === gridNum - 1

If isTopRow is true we cannot move up, and if isBottomRow is true we cannot move down. If isLeftColumn is true we cannot move left, and if isRightColumn if true we cannot move right.

Note: isBottomRow doesn’t only check if the active element is on the bottom row, but also checks if there’s an element beneath it. In our example above, the active element is not on the bottom row, but doesn’t have an item beneath it.

A Working Example

I’ve worked this into a full example that works with resizing – and made the #grid element resizable so it can be tested in the snippet below.

I’ve created a function, navigateGrid that accepts three parameters:

  • gridSelector – a DOM selector for the grid element
  • activeClass – the class name of the active element
  • direction – one of up, down, left, or right

This can be used as 'navigateGrid("#grid", "active", "up") with the HTML structure from your question.

The function calculates the number of rows using the offset method, then does the checks to see if the active element can be changed to the up/down/left/right element.

In other words, the function checks if the active element can be moved up/down and left/right. This means:

  • can’t go left from the left-most column
  • can’t go right from the right-most column
  • can’t go up from the top row
  • can’t go down from the bottom row, or if the cell below is empty
const navigateGrid = (gridSelector, activeClass, direction) => {
  const grid = document.querySelector(gridSelector);
  const active = grid.querySelector(`.${activeClass}`);
  const activeIndex = Array.from(grid.children).indexOf(active);

  const gridChildren = Array.from(grid.children);
  const gridNum = gridChildren.length;
  const baseOffset = gridChildren[0].offsetTop;
  const breakIndex = gridChildren.findIndex(item => item.offsetTop > baseOffset);
  const numPerRow = (breakIndex === -1 ? gridNum : breakIndex);

  const updateActiveItem = (active, next, activeClass) => {
    active.classList.remove(activeClass);
    next.classList.add(activeClass); 
  }
  
  const isTopRow = activeIndex <= numPerRow - 1;
  const isBottomRow = activeIndex >= gridNum - numPerRow;
  const isLeftColumn = activeIndex % numPerRow === 0;
  const isRightColumn = activeIndex % numPerRow === numPerRow - 1 || activeIndex === gridNum - 1;
  
  switch (direction) {
    case "up":
      if (!isTopRow)
        updateActiveItem(active, gridChildren[activeIndex - numPerRow], activeClass);
      break;
    case "down":
      if (!isBottomRow)
        updateActiveItem(active, gridChildren[activeIndex + numPerRow], activeClass);
      break;  
    case "left":
      if (!isLeftColumn)
        updateActiveItem(active, gridChildren[activeIndex - 1], activeClass);
      break;   
    case "right":
      if (!isRightColumn)
        updateActiveItem(active, gridChildren[activeIndex + 1], activeClass);    
      break;
  }
}
.grid {
  display: flex;
  flex-wrap: wrap;
  align-content: flex-start;
  width: 400px;
  background-color: #ddd;
  padding: 10px 0 0 10px;
  margin-top: 5px;
  resize: horizontal;
  overflow: auto;
}

.item {
  width: 50px;
  height: 50px;
  background-color: red;
  margin: 0 10px 10px 0;
}

.active.item {
  outline: 5px solid black;
}
<button onClick='navigateGrid("#grid", "active", "up")'>Up</button>
<button onClick='navigateGrid("#grid", "active", "down")'>Down</button>
<button onClick='navigateGrid("#grid", "active", "left")'>Left</button>
<button onClick='navigateGrid("#grid", "active", "right")'>Right</button>

<div id="grid" class="grid">
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item active"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
</div>

Leave a Comment

Hata!: SQLSTATE[HY000] [1045] Access denied for user 'divattrend_liink'@'localhost' (using password: YES)