Резиновое меню из блоков

Автор: Андрей Косяк и Евгений Рыжков Дата публикации: 17.06.2011

Задача

Сделать резиновое меню, которое занимает всю доступную ширину родителя. При этом пункты меню должны быть прижаты друг к другу: меню как бы в виде блоков.

пример резинового меню из блоков

Требования

  • семантический код на списках (никаких таблиц);
  • без использования Javascript и серверных вычислений;
  • возможность добавлять/уменьшать количество пунктов меню с сохранением первоначального дизайна;
  • адаптация под разные разрешения при резиновых дизайнах.

Решение (вариант 1)

Для решения такой задачи идеально подходит таблица со своей способностью «приспосабливаться к внешним условиям». Каких-либо других элементов, которые обладали бы данной особенностью я не знаю. Единственное, что пришло на ум — это эмуляция поведения таблицы для списка. В этом нам поможет CSS свойство display и его значения table-row и table-cell. И так, в HTML у нас обычный список:

<nav>
<ol id="nav"> <!-- id тут нужен только для IE6-7 и только для резиновых дизайнов -->
<li><a href="#">Главная</a></li>
<li><a href="#">Собираем</a></li>
<li><a href="#">Без цензуры чхать на цензуру</a></li>
<li><a href="#">Учимся</a></li>
<li><a href="#">Справочники</a></li>
</ol>
</nav>
ol {
	display: table-row; /* эмуляция строки таблицы */
}
li {
	width: auto; /* чтобы поведение было аналогичным ячейки таблицы */
	display: table-cell; /* эмуляция ячейки таблицы */
	text-align: center;	
	height: 50px; /* высота пункта */
	padding-left: 2px; /* визуальная граница между пунктами меню */
	vertical-align: bottom; /* это нужно для случаев, когда в пункте меню текста больше чем на одну строку */
}
li:first-child {
	padding: 0; /* у первого элемента убираем отступ чтобы четко прилег к левой границе */
}
a {
	width: 1000px; /* вот это заставляет наши псевдоячейки растянуться должным образом */
	height: 50px;
	display: table-cell; /* без этого тоже никак */
	vertical-align: middle; /* вертикальное выравнивание текста */
}

Этого достаточно для большинства популярных ныне браузеров. Но так как IE6 и 7 все еще присутствуют на рынке, без «шаманства» не обойтись. Во-первых, не забываем подключать html5shiv или modernizr, если используется HTML5 разметка. Во-вторых, пишем еще приличный кусок CSS кода для этих браузеров (только не забудь вынести его в отдельный CSS и подключить только для IE нужных версий):

nav { /* эти правила нужны чтобы устранить ряд небольших багов ие */
	display: block;
	width: 100%;
	overflow: hidden;
	position: relative;
}

li {
	display: inline; /* table-cell мы же не понимаем */
	zoom:1;
	overflow: hidden;
}

ol {
	display: block;
	position: relative;
	height: 50px;	
	overflow: hidden;
	/* ниже идет мега шамантсво, где рассчитывается ширина пункта меню в зависимости от текущей ширины меню и к-ва пунктов в нем */
	z-index: expression(runtimeStyle.zIndex = 1, function(node) {	
		var	list = node.childNodes, i = list.length, iWidth = node.offsetWidth/i;
	
			while(i--) {
				list[i].style.width = iWidth+&px&;
			}
			node.style.width=node.offsetWidth+10+"px"; /* 10 - это значение подбираем по обстоятельствам. нужно чтобы меню выстроилось в одну строку (устраняем погрешности округлений вычислений) */
		}(this));)
}
a {
	width: auto;
	height: auto;
	display: block;
	z-index: expression(runtimeStyle.zIndex = 1, this == ((50/2)-parseInt(offsetHeight)/2) <0 ? (style.paddingTop="0",style.height="50px") : (style.paddingTop=(50/2)-(parseInt(offsetHeight)/2) +&px&, style.height="50px")); /* вертикальное выравнивание */
}

И это еще не все. IE6 нужно еще научить понимать first-child. А если дизайн у нас резиновый, тогда нам нужно пересчитывать ширину пунктов меню при изменении размеров экрана:

window.onresize = function()
{

	var nav = document.getElementById("nav");
	nav.style.width = "100%";
	var	list = nav.childNodes, i = list.length, iWidth = nav.offsetWidth/i;
	
			while(i--) {
				list[i].style.width = iWidth+&px&;
			}
			nav.style.width=nav.offsetWidth+10+"px";
}

Демо пример. Проверено в:

  • IE 6-9
  • Firefox 4
  • Opera 11
  • Safari 5
  • Chrome
  • iPhone 3GS

Если код копируешь из примеров, не забудь удалить комментарии из CSS — старые IE не всегда адекватно на их реагируют.

P.S.

Хотя для старых IE решение громоздко, изобилует экспрешенами, все же это работает достаточно быстро даже в 6-м. И я придерживаюсь мнения, что лучше сайт будет работать медленнее на старых браузерах, но лучше на новых.

display:table VS display:table-row

Update 17.06.2011 by Александр Головко.

В комментариях у Seva возник резонный вопрос, почему собственно используется достаточно экзотическое display: table-row, если можно прописать display: table и в этом случае избавится от скользкого правила:

a {
width: 1000px;
}

Почему скользкого? Потому что оно не позволит задать ссылке правый паддинг. А когда речь идет о красиво оформленном меню на макете, это может быть важно.

Для того чтобы этот вопрос решить, я модифицировал первоначальный демо-пример. Результат получился довольно неоднозначный.

Фактически вышло три варианта решения — ol, представленный в табличном виде можно принудительно растянуть на всю ширину (что для table-row получалось автоматически), а можно и не растягивать.

И в том и в другом случае результат отличается от базового (с table-row) тем, что пункты меню для всех нормальных браузеров будут неодинаковыми по ширине (пропорционально своему содержимому). Конечно, в IE7 все будет одинаково, ты ведь помнишь, там это обеспечивает шаманство, с которым не поспоришь.

Третий случай получаем, если все-таки растянуть ссылки — насколько я могу судить, результат тогда не отличается от table-row.

Чтобы окончательно понять разницу, посмотри демо пример со всеми тремя вариантами. Обрати внимание, как ведет себя меню при маленьком и большом разрешении. Я специально сделал затемнение фона ссылки при ховере, чтобы были видны слабые места полученных результатов.

Мораль примерно такова: хочешь пункты меню одинаковыми по ширине — используй метод с table-row, хочешь разные, можно писать display: table. В общем, имеем разные варианты, каждый выбирает себе по душе (или по дизайну).

Вариант 2: display:table

Update 18.06.2011 by Seva.

Я не совсем это имел ввиду... Для всех случаев только table. Объясню детальнее, почему table, а не table-row.

Есть понятие анонимных объектов таблицы, они в определённых ситуациях добавляются. Когда ставим table-row и даём ей 100% то создаётся анонимный объект table (вокруг table-row) у которого нет никакой ширины и по дефолту не 100% поэтому table-row не от кого отсчитать ширину поэтому получается не 100%, а также, по-моему, table-row нельзя задать ширину она равна ширине таблицы (а для таблицы так как это в данном случае анонимный объект не указана ширина). Поэтому и пихаются эти своеобразные костыли-распорки:

a {
  width: 1000px;
}

кстати с таким же успехом можно и

li {
  width:1000px;
}

написать, а с ссылкой делать то что нужно падинг задать или блоком сделать чтоб на всю ширину, что угодно... Но это всё равно не красиво! Так как во-первых width:1000px; это явный костыль, во вторых, чисто теоретически, возможна ситуация что кто-то искусственно задаст очень большую ширину их родителю и 1000px будет мало )))

В принципе при display:table эти костыли тоже можно ставить, только они не нужны потому что:

Если мы ставим display:table; то между table и table-cell создастся анонимный объект: строка(table-row) может ещё и tbody не важно, а важно то что таблице мы можем задать 100% и ячейки сами подстроятся под нужную ширину, и вот тут есть два варианта:

  1. мы хотим, чтобы ячейки были шириной относительно в них содержащегося контента.
  2. мы хотим равные по ширине ячейки...

В первом случае ничего не трогаем оно так и получается по дефолту, для уверенности можно написать для ol (или ul) table-layout: auto хотя оно по дефолту стоит.

Во втором случае есть несколько вариантов сделать распорку width:1000px; для <li> или <a> но это криво. Намного лучше написать просто table-layout: fixed для ol (или ul) и все.

Кстати во всех этих способах есть ещё одна важная деталь!

Мы можем играться со свойством border-collapse: separate; (оно по дефолту стоит вроде) то есть у нас по две рамки между пунктами меню можем менять их, и расстояние между ними почти как и при работе с блоками только вместо марджина (марджин ячейкам нельзя ставить) юзаем border-spacing для элементов <ol> (или <ul>). Если это не нужно, то делаем обычную border-collapse: collapse; и у нас будут общие рамки у элементов <li>.

Демо пример (Примечание редакции: осторожно — пример не снабжен костылями для IE7!).

По теме