Search code examples
csscss-multicolumn-layout

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


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>


Solution

  • 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>