Add equal amount of padding to anchors in a fluid horizontal list

I’ve spent quite a bit of time looking at this, and I think it’s probably not possible with CSS. But if someone can prove me wrong, that would be great!

What I’m looking to do is to have a horizontal navigation menu where there is an equal spacing between each item, but each item may be a different size. The menu needs to be fluid, so that the gap between each item reduces / increases as the page width reduces / increases.

The justify trick detailed here: http://stackoverflow.com/questions/6865194/fluid-width-with-equally-spaced-divs is along the right lines. But when I say an equal spacing between the menu items, what I really mean is that I want equal padding applied to the anchors that make up the list. There should be no actual spacing between the list items.


<ul>
	<li><a href="#">Option One</a></li>
	<li><a href="#">Option Two</a></li>
	<li><a href="#">This is option three</a></li>
	<li><a href="#">Opt 4</a></li>
	<li><a href="#">Option 5</a></li>
</ul>

The nearest I’ve been able to get so far is using the CSS3 calc function, but this requires knowing the number of items in the menu, and the width of each item, which is not really realistic. http://jsfiddle.net/TN9hf/2/embedded/result/

ul{
	list-style-type: none;
  width: 80%;
}
li{
	display: inline-block;
    text-align: center;
    padding: 0;
    margin: 0;
    font-size: 105%;
}
/*combined width of list items with no padding = 79+161+86+60+135=521px*/
li:nth-child(1){
	width: calc((100% - 521px)/5 + 79px);
}
li:nth-child(2){
	width: calc((100% - 521px)/5 + 161px);
}
li:nth-child(3){
	width: calc((100% - 521px)/5 + 86px);
}
li:nth-child(4){
	width: calc((100% - 521px)/5 + 60px);
}
li:nth-child(5){
	width: calc((100% - 521px)/5 + 135px);
}
li a{
    display: block;
    padding: 5px 0;
    background: #999;
}

I believe that the answer is that it can be done with JavaScript but not with CSS. I don’t know how to write the JavaScript, but the concept seems pretty simple. @Paul_O_B is around today. He could provide a definitive answer.

Hi,

I don’t think it is possible in an automatic way based on the criteria that you need,

The justify trick is the way I would have tackled this and indeed the method was first discovered in the Sitepoint forums in a quiz I set some years ago.

However it seems that you don’t want space but that you want each element to butt up to the next. You can get close using display:table and table-cell but it only equalises the cells and not based on the content. If you use table-layout:fixed then the cells are equal but in the table-layout:auto mode you get more spaced out elements but not an exact match for each unfortunately. I also thought that flexbox was supposed to do this but on testing it seems that it also equally distributes the items based on a set width for each.

e.g.


<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Untitled Document</title>
<style type="text/css">

ul{
  display: -moz-flex;
 -moz-justify-content:space-between;
	display: -webkit-flex;
 -webkit-justify-content: space-between;
	justify-content:space-between;
	display:flex;
	list-style:none;
	margin:0 auto;
	padding:0;
	width:80%;
	border:1px solid #000;
}
ul li {
	line-height:30px;
	-webkit-flex: 1;
	-moz-flex: 1;
	flex: 1; 
	text-align:center;
	width:100%;
}
ul li a{display:block;background:blue;color:red}
a:hover { background:green }



</style>
</head>

<body>
<ul>
		<li><a href="#">Option One</a></li>
		<li><a href="#">Option Two</a></li>
		<li><a href="#">This is option three</a></li>
		<li><a href="#">Opt 4</a></li>
		<li class="last"><a href="#">Option 5</a></li>
</ul>

</body>
</html>

There may well be a setting that will satisfy the requirements but flexbox is still buggy and not widely supported.

I think the best you can hope for with automatic sizing is using display:table and display table-cell but they don’t produce evenly spaced elements based on content.


<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Untitled Document</title>
<style type="text/css">
ul {
	display:table;
	list-style:none;
	margin:0 auto;
	padding:0;
	width:80%;
	border:1px solid #000;
}
ul li {
	line-height:30px;
	display:table-cell;
	text-align:center;
}
ul li a {
	display:block;
	background:blue;
	color:red;
	border-left:1px solid #000;
	border-right:1px solid #000;
}
a:hover { background:green }
</style>
</head>

<body>
<ul>
		<li><a href="#">This is option One</a></li>
		<li><a href="#">Option Two</a></li>
		<li><a href="#">This is option three</a></li>
		<li><a href="#">Opt 4</a></li>
		<li class="last"><a href="#">Option 5</a></li>
</ul>
</body>
</html>


Of course that just renders larger text lines with larger space so is not equal as such and the alternative table-layout:fixed method just makes all cells the exact same width.

You may be able to achieve this using flex-box, but that wouldn’t make your design very cross browser friendly (then again , neither is calc(); ) otherwise i believe you will need .js

UPDATE:

Paul ninja’d me and with a better answer. The true issue here is we are not really targeting the element, but the space between.

Thanks for the replies everyone. I think that using js with table-cell display for non-js users is going to be the solution.

Yes, that’s exactly the solution I was expecting. If you already have the JavaScript, I would be grateful if you would post it. Otherwise, I intend to try to write it (or get some help doing so) because this is a repeated request and I’d like to have the solution “at the ready”. :slight_smile:

Cheers

I haven’t written the js yet, but I’ll post it here when done.

Thanks, I’ll keep an eye open for it. :slight_smile:

Meanwhile feast your eyes on this version. It’s pure CSS and automatically justifies depending on the text length while also stretching the target area to the edges.

It works by using the span as an inline-block which gets justified but still leaves the anchor element as an inline element. Some generated content is then added to the anchor causing the inline anchor element to also justify and spread out to the full width. The whitespace gaps have to be killed by butting the tags together. It’s a tricky combination and needs a couple of non breaking spaces to trigger the justify behaviour. (It may be possible to remove the non breaking spaces and use generated content instead but I couldn’t get it working in the short time I had.

Unfortunately it only really works in Firefox although I have had IE and chrome working separately at times with variations of the code. It’s too buggy and tricky to use for real though unless it can be packaged more neatly.

Useful as an exercise in what can be done though :wink:

Here’s the js I promised. I’m afraid that it’s tailored to my use, e.g. I have a bit in there that modifies some css specific to how my menu is styled. It also won’t work if your ul or lis have padding or margin applied. So I’m not sure how useful it will be to anyone. But at least it should provide a starting point if you want a similar function.

It doesn’t work in IE6 or 7, you could add a conditional statement to set the display to inline instead of inline-block in IE if you wanted support for those browsers. Sometimes in IE the end item drops off the end of the menu onto the next line, but I haven’t had time to debug this yet.

Since the site I’m doing this for doesn’t really need jQuery, I decided to do it in raw js, which means I needed a couple of helper functions. So I stole Craig Buckler’s each function from his series of articles on Native JavaScript Equivalents of jQuery Methods.

I might try and write a more universal function with an example page sometime, but unfortunately I don’t have the time at the moment.

(function(){

/**loop through an array / list, passing each item in the list to the callback function
 *The callback function receives two arguments, the current item in the list, and the index of that item in the list
 *If the callback function returns false, then the loop will exit
 *(Taken from http://www.sitepoint.com/jquery-vs-raw-javascript-3-events-ajax/)
 *@param obj The array or nodeList to loop through
 *@param fn The callback function that each item in the list will be passed
 */
function Each(obj, fn) {
	if (obj.length) for (var i = 0, ol = obj.length, v = obj[0]; i < ol && fn(v, i) !== false; v = obj[++i]);
	else for (var p in obj) if (fn(obj[p], p) === false) break;
};

/**
 *Cross-browser method to attach an event listener
 *@param HTMLObject el The element to attach the event listener to
 *@param string eventType The type of event to listen for
 *@param function fn The function to fire when the event occurs
 */
function addEvent(el, eventType, fn) {
    if (el.addEventListener) {
        return el.addEventListener(eventType, fn);
    }
    else if (el.attachEvent) {
        return el.attachEvent('on'+eventType, fn);
    }
}

/**
 *Space items equally across a horizontal menu so that they fill the whole menu by applying
 *the same amount of padding to the anchor element contained in each list item.
 *@param HTMLObject The ul or ol html object that is the menu
 *@param number The minimum amount of padding to apply
 */
function justifyMenu(menu,minPadding) {
    //check we have an ol or ul menu
    if (menu.tagName != 'UL' && menu.tagName != 'OL') {
        throw new TypeError('menu should be an unordered or ordered list HTML Object');
    }
    //initialise some variables
    var lis = menu.children,
    as=[],
    contentWidth=0,
    menuWidth;
    //set minPadding to a default value of 5 if not provided
    minPadding = (typeof minPadding == 'number') ? minPadding : 5;
    //set the list items to wrap to their contents and get their width
    Each(lis, function(as){return function(el){
        el.style.display='inline-block';
        //for IE7 set display to inline and add zoom: 1
        
        //get the child anchor tag
        Each(el.children, function(as){return function(el){
            if (el.tagName == 'A') {
                //reset the anchor padding to the minimum
                el.style.paddingLeft=minPadding+'px';
                el.style.paddingRight=minPadding+'px';
                //add the anchor to the array of anchors to have their padding modified
                as.push(el);
            }
        }; }(as))
        //now we can get the width of the list item
        contentWidth+=el.offsetWidth;
        
    };}(as));
    //only justify the menu items if they are smaller with the minimum padding applied than the menu width
    if (contentWidth < (menuWidth=menu.offsetWidth)) {
        //Taking the contentWidth away from the menuWidth gives us the total spare space
        //Dividing the spare space by the number of list items gives us the total padding to apply to each item
        //Dividing this by 2 gives us the amount of padding to apply left and right
        //We need to round the value down otherwise IE9 & 10 will suffer from rounding issues, pushing the final list item off the end and onto a new row
        var padding=Math.floor((menuWidth-contentWidth)/(lis.length*2))+minPadding;
        Each(as, function(el){
            el.style.paddingLeft=padding + 'px';
            el.style.paddingRight=padding + 'px';
        });
        //Fix the display of the submenus now we are using inline-block display for the parents instead of table-cell (see ".main-navigation li ul" ruleset in theme's style.css)
        Each( menu.getElementsByTagName('ul'), function(el){
            var s=el.style;
            s.position='absolute';
            s.top='100%';
            s.zIndex = 1;
            s.height = 'auto';
        });
    }
}

//don't justify the menu for IE7
if(document.getElementsByTagName('html')[0].className.indexOf('ie7') == -1){
    justifyMenu(document.getElementById('myMenu'));
    addEvent(window, "resize", function(e) {
        justifyMenu(document.getElementById('myMenu'));
    });
}



})();

This is very cool, Paul. Kinda mind boggling. Is the flaw between browsers due to a lack of standards compliance or is this “uncharted territory”? I learned through experience a couple of weeks ago that just because something works nicely in Firefox doesn’t mean that it’s standards compliant. Quite the opposite, in fact. That’s why I wonder what is causing the disparity between browsers. It’s an amazing feat even if it’s “edgy”. :slight_smile:

Your JavaScript is far more sophisticated than my simplistic vision was/is. With a little more refining, yours could probably become nearly plug-n-play in modern browsers. Very impressive. If I can get my simple version to work, I’ll post it for its entertainment value; but so far, it’s a no-go. I’m still trying to get yours to work, actually. :slight_smile:

Thanks very much for posting this. I appreciate the skill that you are demonstrating. :cool: :agree:

When I have a bit more time I’ll make up an example page, as my js probably doesn’t make a lot of sense without the html and css. Unfortunately I can’t easily just pull out the relevant html and css from the project I’m using this for as it’s a wordpress child theme, so there are some CSS rules over-riding the parent theme CSS rules and gets a bit tricky trying to figure it all out.

Paul’s example looks promising, on the top menu the last option drops off (well the anchor stays but gets small and the text disappears) in FF 22 for me, but the others are okay. I’ll have to take a proper look at it later. Thanks for posting it Paul!

It’s not really viable for production in that state so don’t spend too long on it.:slight_smile: I’ll need to revisit it and see if I can make it more stable but I thin we’re at the limits of what we can do here with css alone.

The browsers are pretty consistent with the demo except for the start and end positions of the menu. The menu works fine as a justified menu but like justified text the first word and last word would be at the end of the line. The hacks come into play when trying to move the first and last item away from the edge to match the rest. Theoretically an invisible character at the start and end of the line should give the effect needed and indeed does work in Firefox but other browsers don’t do it exactly and need more characters. There may be an optimum to suit all browsers but needs more testing.:slight_smile:

I’ve had a bit of time to get a test page up with the javascript now: http://www.xoogu.com/files/2013/07/justified-menu-of-mu-mu.html

There are still quite a few things I need to work out, and I’ve only tested this on Firefox (it’s modified from what I used for the site I was on working on). But at least you should be able to see it in action.

Strangely, when the total width of the list items exactly match the space available, firefox pushes the last item onto a new line:

You could just add a negative right margin of 2px to the last list item to stop it dropping.

e.g.


ul li:last-child{margin-right:-2px}

I’ve done a bit more work on this now, and it should work in most browsers, but there are still a few issues. Same link as before: Justified menu example

The submenus are messed up in IE 7 (and won’t work in IE6). Rather than trying to fix it, I have just disabled them in these browsers.

It doesn’t work in IE6. If you need IE6 support, it can be modified by setting the anchors’ display to inline and zoom to 1, e.g.

//get the child anchor tag
            Each(el.children, function(as){return function(el){
                if (el.tagName == 'A') {
                    //reset the anchor padding to the minimum
                    el.style.paddingLeft=minPadding+'px';
                    el.style.paddingRight=minPadding+'px';
                    if(IE==6){
                        el.style.display='inline';
                        el.style.zoom=1;
                    }
                    //add the anchor to the array of anchors to have their padding modified
                    as.push(el);
                }
            }; }(as))

With javascript disabled I have had to use whitespace: nowrap to prevent the text in each menu item from wrapping. If the text wraps onto a new line, then the item has a larger height than the other items. I have tried to fix this, but have not been able to find a solution.

The sub menus fit to the width of their parent. Ideally they would expand their contents so that the text does not wrap (or overflow). However, I have not been able to find a solution for this either.

I haven’t looked at Paul’s idea or flexbox properly yet.

I’ve had a look at Flex box and Paul’s example, refined the javascript solution, and written it all up into a blog post: http://www.xoogu.com/2013/justified-horizontal-menu-css-javascript-solutions/

Hope it helps someone. If anyone wants to propose an alternative solution or point out mistakes I’ve made / things that could be done better, that would be great.

Good write up. Well done :slight_smile: I’m sure it will help a few people.

As an aside I see you mention IE10 not working with flexbox and the reason is that IE10 uses the old syntax and IE11 uses the new syntax.

Thanks for the tip Paul. I’ve added an update to the article now.

With the correct flexbox syntax for IE10, it renders the menu the same way as a fixed layout table, which is different to Chrome and Firefox.