简体   繁体   中英

Distribute elements evenly using CSS

A method to distribute elements evenly in a container using CSS appeared on Smashing Magazine today.

I recently had to use Javascript to achieve the same effect for elements of variable width, but the method presented on SM made me wonder if it was possible to do this without Javascript.

There's this question , where gargantaun says:

IMHO, and you probably don't want to hear this, but the design is probably flawed. It's common knowledge that distributing items evenly across a layout with CSS is a pain, so designers should avoid it.

But I can't tell the designer to change his design, and I don't agree that the shortcomings of CSS should limit designers.

Anyway, here's what I have in HTML (translated and simplified):

<div id="menu">
    <ul>
        <li><a href="/">Home</a></li>
        <li><a href="/news">News</a></li>
        <li><a href="/theme">Theme</a></li>
        <li><a href="/activities">Activities</a></li>
        <li><a href="/contact">Contact</a></li>
    </ul>
</div>

in CSS (irrelevant properties removed and simplified):

#menu li { float: left; margin-right: 20px; }
#menu a  { display: block; padding: 0 1em; }

and in Javascript:

function justifyMenu() {
    var menuItems  = $$("#menu li");
    var menuWidth  = $("menu").getWidth();
    var totalWidth = 0;

    menuItems.each(function(e) {
        totalWidth += e.getWidth();
    });

    var margin = (menuWidth - 4 - totalWidth) / (menuItems.length - 1);
    margin = parseInt(margin);

    menuItems.each(function(e) {
        e.setStyle({ marginRight: margin + 'px' });
    });

    menuItems[menuItems.length - 1].setStyle({ marginRight: '0' });
}

And here's a scaled-down screenshot (see how the menu aligns with the header image):

截图

Any idea how I can achieve this without Javascript?

Of course this is exactly what the table element is for. It's sad and hilarious at the same time to see people twist themselves into a gordian knot with CSS, most of them not even knowing why they're avoiding tables.

Whatever reason you might have dreamed up to reject tables, it can't possibly be worse than depending on Javascript to layout your page.

Yes, I know this is not the answer you were looking for, but golly, it's so obvious .

There have been casual claims that tables are the obvious solution, however, there hasn't been any real discussion of how to implement it. I'll show you that displaying divs as a table is the right way to do this, but it is not as easy as centering all of the cells and setting an automatic width. The problem with this is that you have no control of the outer margins of the further-most left and right cell-contents. They both are inset from its containing box an arbitrary amount you cannot control. Here's a work around:

First, a slight modification of Guder's html:

<div id="menu">
    <ul>
        <li class="left"><a href="/">Home</a></li>
        <li><a href="/news">News</a></li>
        <li><a href="/theme">Theme</a></li>
        <li><a href="/activities">Activities</a></li>
        <li><a href="/contact">Contact</a></li>
    </ul>
</div>

Now the css:

#menu {display:table; width:// some width in px //}
#menu ul {display:table-row; width: 100%}
#menu li.left {display: table-cell; text-align: left; width: 20px; white-space:nowrap;}
#menu li {display: table-cell; text-align: right; width: auto;}

Now, we have full control of the outer-most sides of the menu, which align with the far-left and far-right sides of the containing box, and the distance between each element is consistent. You'll notice that I used a trick to get the furthest left cell to be the exact-width of it's content. I set the width property to a small size obviously below what its contents would normally be. I then set the white-space to no-wrap, which stretches the cell the least amount to fit the text of the cell. You can see here an image which shows the effect of this (using different html elements):

在猫身上徘徊

The beauty of this code is that it can accept however many cells and text-widths, without any knowledge of their actual widths, and distribute them evenly across the page. All the while, left and right elements reaching their perimeters, and ofcourse we have all our html in divs, no browser or internet geek is mislead to believe we're presenting tabular data. No known compromises here!

This is what display:table-cell is supposed to achieve - however, the IE's just don't do it, and FF<3 has problems with it too, I believe.

These styles work in FF3, Chrome, Safari, and (I think) Opera 9:

#menu ul {display:table;padding:0;}
#menu li {display:table-cell;text-align:center;}

But you'll need a fair few hacks to get them working in the usual, commercial set of browsers.

Even though Colin Brogan's answer provides solid foundation to approach a "almost there" resolution to the problem, it still depends on text length. If text is too long, the "cell" will be wider and thus have more space on the left. I tried to address the problem based on the code presented in his answer, but I concluded that the problem has not a real possible solution with tables or fake-tables (display:table-cell).

So we'll have to wait for CSS3 flexible box model to be more widely supported (you can check updated support here ). In the meantime, you can use the Flexie polyfill to patch browsers that don't support it. If you want to check how it'll look like on WebKit now (without needing polyfill), you can try the following CSS:

#menu ul {
  display: -webkit-box;
  -webkit-box-orient: horizontal;
  -webkit-box-pack: justify;
  width: 940px;
  list-style: none;
  padding: 0;
  border: 1px solid gray;
}
#menu li {
  border: 1px solid silver;
}

Notice it only uses WebKit prefixes. You should add prefixes for the other browsers aswell if you decide to take it to production website.

This approach does accept an unknown amount of items and text-widths, without any knowledge of their actual widths , and distribute them evenly across their container (in this case, #menu ul ).

If you decide to be conservative, the approach suggested by Colin Brogan is the most acceptable given that you keep your texts on the same approximately length. If not, wider spaces will start to show.

Yes, you can do it, as long as the widths of the elements to be distributed are known in advance. But it's a bit messy.

The trick is, you want a spacing between each element of '(Wp-sum(Wc))/(Nc-1)', that is width of the parent element minus the total width of all the child elements, divided equally between the number of gaps between the elements.

Because CSS doesn't have the ability to do expressions, we have to hack it a bit. First we add a margin to the parent element of the size 'sum(Wc)', the total width of all child elements. So now the parent has width '(Wp-sum(Wc))', and we can use a padding value in % relative to that width.

So for example, for four images of sizes 10px, 20px, 40px and 80px respectively, our 'sum(Wc)' is 150px. Set that as the parent margin, then the children can have one-third of that width as padding between them.

<style type="text/css">
    #nava { width: 10px; height: 20px;}
    #navb { width: 20px; height: 20px;}
    #navc { width: 40px; height: 20px;}
    #navd { width: 80px; height: 20px;}

    #nav { margin-right: 150px; white-space: nowrap; }
    #nava, #navb, #navc { padding-right: 33.3%; }
</style>

<div id="nav"
    ><img id="nava" src="nava.png" alt="a"
    ><img id="navb" src="navb.png" alt="b"
    ><img id="navc" src="navc.png" alt="c"
    ><img id="navd" src="navd.png" alt="d"
></div>

The funny tag indentation is to avoid there being any whitespace between images. 'nowrap' is necessary because with the parent width set narrower than the page width, it wouldn't otherwise be possible to fit all the elements on the row. Finally, in IE you may need to add a wrapper div around the lot with 'width: 100%; overflow: hidden' to prevent unwanted scrollbars if you're spanning the whole page. And certainly you'll want to be in Standards Mode.

This can work with textual elements too, if you make them inline blocks so you can add padding, and you size them explicitly in ems. It won't work if the sizes of the child elements are not known in advance (eg. they contain dynamic content), as you won't know the 'sum(Wc)' value to use.

To be honest I would probably just use a table. The table layout algorithm copes very smoothly with calculating how to distribute spare table width. (Use 'table-layout: fixed' for best results with known-width cells, or 'auto' to respond to dynamic contents.) This way you also don't have to worry about pixel rounding errors.

If you were using text-based sizes (em, ex) it'd be a lot easier. You can then deal in letters rather than pixels.

Example: The whole thing is 30 capital letter Ms wide. You can then use the width of each nav element (based on its textual content) and do your math statically from there.

The top answer didn't work for me, and GarciaWebDev's answer won't do it for me yet because I need to support a few other browsers, including IE8.

This method worked for me. The idea is to make a containing element text-align: justify and to make the elements to distribute display: inline-block.

HTML:

<div id="menu">
    <ul>
        <li><a href="/">Home</a></li>
        <li><a href="/news">News</a></li>
        <li><a href="/theme">Theme</a></li>
        <li><a href="/activities">Activities</a></li>
        <li><a href="/contact">Contact</a></li>
        <li class="filler"></li>
    </ul>
</div>

CSS:

#menu {
    text-align: justify;
}

ul {
    margin: 0;
    padding: 0;
}

#menu li {
    display: inline-block;
}

.filler {
    width: 100%;
    height: 0;
}

Here is the code in jQuery format for anyone who finds it useful

function justifyClients() {
                        var menuItems  = $("#clients-wrapper ul li").get();                         
                        var menuWidth  = $("#clients-wrapper ul").width();                          
                        var totalWidth = 0;

                        $("#clients-wrapper ul li").each(function(i,e)
                                        {
                                            totalWidth += $(e).width();
                                        });

                        var margin = (menuWidth - 4 - totalWidth) / ($("#clients-wrapper ul li").length - 1);
                        margin = parseInt(margin);

                        $("#clients-wrapper ul li").each(function(i,e) {

                            if(i < $("#clients-wrapper ul li").length - 1)
                            {
                                alert(i + " " + $("#clients-wrapper ul li").length);
                                $(e).css('margin-right',  margin);
                            }
                        });
                    }

                    $(document).ready(function() {
                      justifyClients();
                    });

Demo - http://codepen.io/vsync/pen/tFwxu

all you need if to make the list itself text-align:justify and then add some pseudo item top the end of it and make it fill all the width, to trick the list into justifying all it's items across it's total width.

Trevor Dixon's improved variant (without extra <li> )

HTML

<ul>
    <li><a href="/">Home</a></li>
    <li><a href="/news">News</a></li>
    <li><a href="/theme">Theme</a></li>
    <li><a href="/activities">Activities</a></li>
    <li><a href="/contact">Contact</a></li>
</ul>

CSS

ul {
    margin: 0;
    padding: 0;
}
ul li {
    display: inline-block;
    text-align: justify;
}
ul:after{
    display: inline-block;
    content: '';
    width: 100%;
    height: 0;
}

Thanks to the CSS3 Flexbox module , this is possible with two lines of CSS.

Check the Browser compatibility table for Flexbox

HTML

<div id="menu">
  <ul>
    <li><a href="/">Home</a>
    </li>
    <li><a href="/news">News</a>
    </li>
    <li><a href="/theme">Theme</a>
    </li>
    <li><a href="/activities">Activities</a>
    </li>
    <li><a href="/contact">Contact</a>
    </li>
  </ul>
</div>

CSS

ul {
  display: flex;
}
li {
  flex: 1; /* Short hand for flex-grow: 1 and flex-shrink: 1 */
}

Output:

 ul, li { margin: 0; padding: 0; } ul { display: flex; list-style: none; } li { flex: 1; text-align: center; } 
 <div id="menu"> <ul> <li><a href="/">Home</a> </li> <li><a href="/news">News</a> </li> <li><a href="/theme">Theme</a> </li> <li><a href="/activities">Activities</a> </li> <li><a href="/contact">Contact</a> </li> </ul> </div> 

AFAIK there is no way to achieve this just with CSS. Anybody correct me if this is wrong, pls.

If you use the Yahoo! User Interface Library (YUI) grids.css , it might work.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM