How can I insert column break in a CSS multi-column layout?
Asked Answered
T

2

8

I'm trying to implement a mega-menu.

The number of menu items is variable. By default, they have to be rendered in 4 columns, balanced (the number of items on each column should be nearly the same as the other columns). The height of the mega-menu is also variable, based on its content.

I've implemented it with CSS Multi-Column layout.

The code for this is:

.menu {
  -webkit-column-count: 4;
     -moz-column-count: 4;
          column-count: 4;
 -webkit-column-gap: 32px;
    -moz-column-gap: 32px;
         column-gap: 32px;
}

My issue is that there is a special menu item type, that should act as a column break. This menu item type is optional, but if present, it should force the browser to start a new column to display the content (there can be max 3 column breaks).

I've added the following css code:

.menu-item--column-break {
    display: block;
    -webkit-column-break-before: column;
              -moz-break-before: column;
                   break-before: column;
}

But this CSS works only on Chrome:

enter image description here

Firefox & Safari does not support the CSS rules for the "column-break" element and displays it like a normal menu item: enter image description here

The menu is generated in JavaScript from a JSON object, the HTML can be altered, but I prefer a CSS/JS-only solution.

Do you have any idea on how could I implement this in all browsers?

Here's the full code:

https://codepen.io/andreivictor/pen/ywLJKx

or

let items = [
  {title: 'Category 1', type: 'menu-item'},
  {title: 'Category 2', type: 'menu-item'},
  {title: '---cb---', type: 'column-break'},
  {title: 'Category 3', type: 'menu-item'},
  {title: 'Category 4', type: 'menu-item'},
  {title: 'Category 5', type: 'menu-item'},
  {title: 'Category 6', type: 'menu-item'},
  {title: 'Category 7', type: 'menu-item'},
  {title: 'Category 8', type: 'menu-item'},
  {title: 'Category 9', type: 'menu-item'},
  {title: '---cb---', type: 'column-break'},
  {title: 'Category 10', type: 'menu-item'},
  {title: 'Category 11', type: 'menu-item'},
  {title: 'Category 12', type: 'menu-item'},
  {title: 'Category 13', type: 'menu-item'},
  {title: 'Category 14', type: 'menu-item'},
  {title: 'Category 15', type: 'menu-item'},
  {title: 'Category 16', type: 'menu-item'},
  {title: 'Category 17', type: 'menu-item'},
  {title: 'Category 18', type: 'menu-item'},
  {title: 'Category 19', type: 'menu-item'},
  {title: 'Category 20', type: 'menu-item'},
  {title: 'Category 21', type: 'menu-item'},
];

const $menu = document.querySelector('.menu');

console.log( $menu );

items.forEach((item) => {
  let nodeItem = document.createElement("div");
  nodeItem.classList.add('menu-item');
  let nodeItemText = document.createTextNode(item.title);
  nodeItem.appendChild(nodeItemText);
  if (item.type === 'column-break') {
    nodeItem.classList.add('menu-item--column-break');
  }
  $menu.appendChild(nodeItem);  
});
.menu {
  position: relative;
  padding: 0 16px;
  -webkit-column-count: 4;
     -moz-column-count: 4;
          column-count: 4;
  -moz-column-rule: 1px solid #e2e1e1;
       column-rule: 1px solid #e2e1e1;
  -webkit-column-gap: 32px;
     -moz-column-gap: 32px;
          column-gap: 32px;
}

.menu-item--column-break {
    display: block;
    -webkit-column-break-after: column;
    -moz-break-after: column;
    break-after: column;
    color: red;
}
<div class="container">
  <div class="menu">
  </div>
</div>
Trichocyst answered 25/2, 2019 at 19:25 Comment(1)
Please include the minimal reproducible example from CodePen in the question itself.Prickle
D
1

I was thinking about this and came up with another solution. Basically the problem is that multi columns breaking is not supported so it's not possible to create those fixed columns and dynamic columns with just css for all browser at the moment. Therefore I decided to split the problem into two. I separate the items into groups based on the fixed breaks. And I assume each group will be one column for starters. Then I see how many columns I have. If it's less than 4 (the number of the columns you want) then I allow the largest group to break into one more column dynamically. I continue this until I reach the total of 4 columns - be it fixed, or dynamic, or both.

See the snippet below.

Also, play with the snipped by adding, removing or moving the breaks. It should work for many different scenarios.

let items = [
  {title: 'Category 1', type: 'menu-item'},
  {title: 'Category 2', type: 'menu-item'},
  {title: '---cb---', type: 'column-break'},
  {title: 'Category 3', type: 'menu-item'},
  {title: 'Category 4', type: 'menu-item'},
  {title: 'Category 5', type: 'menu-item'},
  {title: 'Category 6', type: 'menu-item'},
  {title: 'Category 7', type: 'menu-item'},
  {title: 'Category 8', type: 'menu-item'},
  {title: 'Category 9', type: 'menu-item'},
  {title: '---cb---', type: 'column-break'},
  {title: 'Category 10', type: 'menu-item'},
  {title: 'Category 11', type: 'menu-item'},
  {title: 'Category 12', type: 'menu-item'},
  {title: 'Category 13', type: 'menu-item'},
  {title: 'Category 14', type: 'menu-item'},
  {title: 'Category 15', type: 'menu-item'},
  //{title: '---cb---', type: 'column-break'},
  {title: 'Category 16', type: 'menu-item'},
  {title: 'Category 17', type: 'menu-item'},
  {title: 'Category 18', type: 'menu-item'},
  {title: 'Category 19', type: 'menu-item'},
  {title: 'Category 20', type: 'menu-item'},
  {title: 'Category 21', type: 'menu-item'}
];

const $menu = document.querySelector('.menu');

var allGroups = [];
var currentGroup = 0;
allGroups.push({ items: [], columns: 1});

function addGroup($menu, group, numberOfColumns){
	let columnItem = document.createElement("div");
  columnItem.classList.add('menu-group');
  if(numberOfColumns === 1){
  	columnItem.classList.add('fixed');
  } else {
  	columnItem.classList.add('dynamic-columns');
  	var style = '-webkit-column-count: ' + numberOfColumns + ';';
  	style += '-moz-column-count: ' + numberOfColumns + ';';
  	style += 'column-count: ' + numberOfColumns + ';';
  	columnItem.setAttribute('style', style);
  }
  group.forEach((groupItem) => {
  	columnItem.appendChild(groupItem);
  });
  $menu.appendChild(columnItem); 
};
var columnsCount = 1;
items.forEach((item) => {
  let nodeItem = document.createElement("div");
  allGroups[currentGroup].items.push(nodeItem);
  nodeItem.classList.add('menu-item');
  let nodeItemText = document.createTextNode(item.title);
  
  nodeItem.appendChild(nodeItemText);
  if (item.type === 'column-break') {
    nodeItem.classList.add('menu-item--column-break');
    //addGroup($menu, currentGroup, 1);
    currentGroup++;
    allGroups.push({ items: [], columns: 1});
    columnsCount++;
  }  
});

var forSorting = [];
allGroups.forEach((item) => { forSorting.push(item); });

while(columnsCount < 4){
	forSorting.sort(function(a, b){
		return (b.items.length/b.columns) - (a.items.length/a.columns);
	});
  forSorting[0].columns++;
  columnsCount++;
}

allGroups.forEach((item) => {
	addGroup($menu, item.items, item.columns);
});
.menu {
  position: relative;
  padding: 0 16px;
  display: flex;
  flex-direction: row;
}

.menu-group:not(:last-child){
  border-right: 1px solid #e2e1e1;
  margin-right: 8px;
}

.menu-group.fixed {
  flex-basis: calc(25% - 8px);
  flex-grow: 0;
  flex-shrink: 0;
}

.menu-group.dynamic-columns {
  flex-grow: 1;
  -moz-column-rule: 1px solid #e2e1e1;
       column-rule: 1px solid #e2e1e1;
}

.menu-item--column-break {
    display: block;
    color: red;
}
<div class="container">
  <div class="menu">
  </div>
</div>
Dexamethasone answered 3/3, 2019 at 8:40 Comment(4)
thanks again! really appreciate it. Unfortunately, it will not work if only one column break is present, and placed near the end on the list. see the following fiddle: jsfiddle.net/pduna76s vs. column-layout codepen.io/andreivictor/pen/wOWWgY...Trichocyst
Hmm. I thought this is how you want this to work. My mistake. Thanks for the jsfiddle above - now I fully get what you are trying to achieve. If I come up with an update to my solution I will let you know :)Dexamethasone
@Trichocyst , I updated the solution based on your last comment and the fiddle you provided. I hope this is now what you wanted :) Let me know if that works for you.Dexamethasone
there are some differences between css column layout and this js implementation (see - in chrome - codepen.io/andreivictor/pen/MxbMxR vs. jsfiddle.net/pacqbf7g) . the css column layout tries to balance the height of all columns. but your solution is the best so far and I will mark it as accepted. thanks a lot for all your help.Trichocyst
D
2

To solve this I decided to go with js approach. It is not fully elegant because in the js code you need to assume you know the height of a single menu item. But this solves the issue and maybe it will fit your project. The idea is that I changed the display of the menu into a flexbox that places items in a column but wraps into the next column when there is no space. Now, to be able to run out of space and wrap we need two things: a fixed menu height (so that is why I calculate it based on the items provided) and also an invisible element that would stretch to 100% height (it does not fit to the current nor the next column so it creates a 0px-wide column on its own thus acting as a column separator). Take a look at this solution:

let items = [
  {title: 'Category 1', type: 'menu-item'},
  {title: 'Category 2', type: 'menu-item'},
  {title: '---cb---', type: 'column-break'},
  {title: 'Category 3', type: 'menu-item'},
  {title: 'Category 4', type: 'menu-item'},
  {title: 'Category 5', type: 'menu-item'},
  {title: 'Category 6', type: 'menu-item'},
  {title: 'Category 7', type: 'menu-item'},
  {title: 'Category 8', type: 'menu-item'},
  {title: 'Category 9', type: 'menu-item'},
  {title: '---cb---', type: 'column-break'},
  {title: 'Category 10', type: 'menu-item'},
  {title: 'Category 11', type: 'menu-item'},
  {title: 'Category 12', type: 'menu-item'},
  {title: 'Category 13', type: 'menu-item'},
  {title: 'Category 14', type: 'menu-item'},
  {title: 'Category 15', type: 'menu-item'},
  {title: 'Category 16', type: 'menu-item'},
  {title: 'Category 17', type: 'menu-item'},
  {title: 'Category 18', type: 'menu-item'},
  {title: 'Category 19', type: 'menu-item'},
  {title: 'Category 20', type: 'menu-item'},
  {title: 'Category 21', type: 'menu-item'},
];

const $menu = document.querySelector('.menu');

console.log( $menu );
var longestColumnLength = 0;
var currentColumnLength = 0;
var numberOfBreaks = 0;

items.forEach((item) => {
    currentColumnLength++;
    let nodeItem = document.createElement("div");
    nodeItem.classList.add('menu-item');
    let nodeItemText = document.createTextNode(item.title);
    nodeItem.appendChild(nodeItemText);
    if (item.type === 'column-break') {
        nodeItem.classList.add('menu-item--column-break');
        let breaker = document.createElement("div");
  	    breaker.classList.add('menu-item--column-break-line');
        $menu.appendChild(nodeItem); 
        $menu.appendChild(breaker); 
        longestColumnLength = Math.max(longestColumnLength, 
           currentColumnLength);
        currentColumnLength = 0;
        numberOfBreaks++;
    } else {
        $menu.appendChild(nodeItem); 
    } 
});

var availableNaturalColumnsAtTheEnd = Math.max(1, 4 - numberOfBreaks);
var maxLengthOfRemainingItems = currentColumnLength / 
      availableNaturalColumnsAtTheEnd;
var actualLongestColumn = Math.max(longestColumnLength, 
      maxLengthOfRemainingItems)

$menu.setAttribute("style", "height: " + actualLongestColumn*20 + "px")
.menu {
  position: relative;
  padding: 0 16px;
  display: flex;
  flex-direction: column;
  flex-wrap: wrap;
  height: 200px;
}

.menu-item{
  height: 20px;
}

.menu-item--column-break {
    display: block;
    color: red;
}
.menu-item--column-break-line {
  height: 100%;
  width: 0;
  overflow: hidden;
}
<div class="container">
  <div class="menu">
  </div>
</div>

Oh, one more thing. I was not sure if you actually want to display the "---cb---" items or not. I left them displayed in the solution but if you want to get rid of them, you can easily modify the code by deleting my extra column dividers, and instead make your "---cb---" items work as the column dividers.

Dexamethasone answered 28/2, 2019 at 7:40 Comment(1)
Thanks for your answer, very interesting with the flexbox approach. But, as you already pointed out, it will work only if the menu items have a fixed height. In my case, the menu items have a different height (because some of them have some margin-bottom, other are displayed as headings, etc).Trichocyst
D
1

I was thinking about this and came up with another solution. Basically the problem is that multi columns breaking is not supported so it's not possible to create those fixed columns and dynamic columns with just css for all browser at the moment. Therefore I decided to split the problem into two. I separate the items into groups based on the fixed breaks. And I assume each group will be one column for starters. Then I see how many columns I have. If it's less than 4 (the number of the columns you want) then I allow the largest group to break into one more column dynamically. I continue this until I reach the total of 4 columns - be it fixed, or dynamic, or both.

See the snippet below.

Also, play with the snipped by adding, removing or moving the breaks. It should work for many different scenarios.

let items = [
  {title: 'Category 1', type: 'menu-item'},
  {title: 'Category 2', type: 'menu-item'},
  {title: '---cb---', type: 'column-break'},
  {title: 'Category 3', type: 'menu-item'},
  {title: 'Category 4', type: 'menu-item'},
  {title: 'Category 5', type: 'menu-item'},
  {title: 'Category 6', type: 'menu-item'},
  {title: 'Category 7', type: 'menu-item'},
  {title: 'Category 8', type: 'menu-item'},
  {title: 'Category 9', type: 'menu-item'},
  {title: '---cb---', type: 'column-break'},
  {title: 'Category 10', type: 'menu-item'},
  {title: 'Category 11', type: 'menu-item'},
  {title: 'Category 12', type: 'menu-item'},
  {title: 'Category 13', type: 'menu-item'},
  {title: 'Category 14', type: 'menu-item'},
  {title: 'Category 15', type: 'menu-item'},
  //{title: '---cb---', type: 'column-break'},
  {title: 'Category 16', type: 'menu-item'},
  {title: 'Category 17', type: 'menu-item'},
  {title: 'Category 18', type: 'menu-item'},
  {title: 'Category 19', type: 'menu-item'},
  {title: 'Category 20', type: 'menu-item'},
  {title: 'Category 21', type: 'menu-item'}
];

const $menu = document.querySelector('.menu');

var allGroups = [];
var currentGroup = 0;
allGroups.push({ items: [], columns: 1});

function addGroup($menu, group, numberOfColumns){
	let columnItem = document.createElement("div");
  columnItem.classList.add('menu-group');
  if(numberOfColumns === 1){
  	columnItem.classList.add('fixed');
  } else {
  	columnItem.classList.add('dynamic-columns');
  	var style = '-webkit-column-count: ' + numberOfColumns + ';';
  	style += '-moz-column-count: ' + numberOfColumns + ';';
  	style += 'column-count: ' + numberOfColumns + ';';
  	columnItem.setAttribute('style', style);
  }
  group.forEach((groupItem) => {
  	columnItem.appendChild(groupItem);
  });
  $menu.appendChild(columnItem); 
};
var columnsCount = 1;
items.forEach((item) => {
  let nodeItem = document.createElement("div");
  allGroups[currentGroup].items.push(nodeItem);
  nodeItem.classList.add('menu-item');
  let nodeItemText = document.createTextNode(item.title);
  
  nodeItem.appendChild(nodeItemText);
  if (item.type === 'column-break') {
    nodeItem.classList.add('menu-item--column-break');
    //addGroup($menu, currentGroup, 1);
    currentGroup++;
    allGroups.push({ items: [], columns: 1});
    columnsCount++;
  }  
});

var forSorting = [];
allGroups.forEach((item) => { forSorting.push(item); });

while(columnsCount < 4){
	forSorting.sort(function(a, b){
		return (b.items.length/b.columns) - (a.items.length/a.columns);
	});
  forSorting[0].columns++;
  columnsCount++;
}

allGroups.forEach((item) => {
	addGroup($menu, item.items, item.columns);
});
.menu {
  position: relative;
  padding: 0 16px;
  display: flex;
  flex-direction: row;
}

.menu-group:not(:last-child){
  border-right: 1px solid #e2e1e1;
  margin-right: 8px;
}

.menu-group.fixed {
  flex-basis: calc(25% - 8px);
  flex-grow: 0;
  flex-shrink: 0;
}

.menu-group.dynamic-columns {
  flex-grow: 1;
  -moz-column-rule: 1px solid #e2e1e1;
       column-rule: 1px solid #e2e1e1;
}

.menu-item--column-break {
    display: block;
    color: red;
}
<div class="container">
  <div class="menu">
  </div>
</div>
Dexamethasone answered 3/3, 2019 at 8:40 Comment(4)
thanks again! really appreciate it. Unfortunately, it will not work if only one column break is present, and placed near the end on the list. see the following fiddle: jsfiddle.net/pduna76s vs. column-layout codepen.io/andreivictor/pen/wOWWgY...Trichocyst
Hmm. I thought this is how you want this to work. My mistake. Thanks for the jsfiddle above - now I fully get what you are trying to achieve. If I come up with an update to my solution I will let you know :)Dexamethasone
@Trichocyst , I updated the solution based on your last comment and the fiddle you provided. I hope this is now what you wanted :) Let me know if that works for you.Dexamethasone
there are some differences between css column layout and this js implementation (see - in chrome - codepen.io/andreivictor/pen/MxbMxR vs. jsfiddle.net/pacqbf7g) . the css column layout tries to balance the height of all columns. but your solution is the best so far and I will mark it as accepted. thanks a lot for all your help.Trichocyst

© 2022 - 2024 — McMap. All rights reserved.