CSS-live.ru

Границы не на всю высоту в списке товаров

Последнее обновление: 27.11.2012.

Совсем недавно я прогуливался по любимому форуму и заострил внимание на одной задачке, которая частенько попадалась мне на глаза. В принципе, ничего сложного она из себя не представляла и мне даже показалось, что её можно решить разными способами. Однако, несмотря на это, многие люди почему-то всё равно приходят с этой проблемой на форум.

Раз такое дело, предлагаю познакомиться с этой задачей поближе и вместе разобрать всевозможные варианты её решения. 

Задача

Речь идёт о списке товаров, пункты которого с некоторых сторон окружены границами. На изображении ниже это прекрасно видно.

Я убрал всё лишнее и оставил саму суть. Видите серые пунктирные линии?  Проблема в том, что, во-первых, у некоторых пунктов эти линии есть не с каждой стороны, а, во-вторых, линии не доходят до краёв и образуют пустые пространства сверху и по бокам пунктов. Думаю, все уже поняли, что от нас требуется? Если так, то поехали!

Светлая мечта

Давайте немного помечтаем. Представьте, что заказчик попросил вас сделать вёрстку основанную только на последних технологиях. Подумайте, как бы вы тогда решали эту задачу?

А пока вы размышляете, я хочу предложить вам своё решение, в котором будет задействован новоиспечённый раскладочный модуль CSS Flexible Box Layout. В данном случае я бы применил именно его, т.к. с его помощью эту задачу можно решить в два счёта.

HTML-код будет выглядеть просто:

<ul class="list">
	<li>
		<div class="img-block"><img src="img1.jpg" /></div>
		<p>HP ProBook Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s,</p>
		<span class="price">$1270.99</span>
	</li>
	<li>
		<div class="img-block"><img src="img2.jpg" /></div>
		<p>Toshiba</p>
		<span class="price">$870.99</span>
	</li>
	<li>
		<div class="img-block"><img src="img3.jpg" /></div>
		<p>Asus</p>
		<span class="price">$60.99</span>
	</li>
	.......
</ul>

Ничего лишнего. Простой список с пунктами. Самое интересное нас ждёт в CSS:

* {
        -moz-box-sizing: border-box;
	box-sizing: border-box;
	padding: 0;
	margin: 0;
}	

.list {
	overflow: hidden;
	margin-left: 3px;
	margin-top: 2px;

	display: flex;
	flex-flow: row wrap;

}
	.list > li {
		position: relative;
		padding: 40px 30px;
		margin-top: -2px;
		margin-left: -3px;
		text-align: center;

		flex: 1 0 33.333%;

	}
	.list > li::after,
	.list > li::before{
		position: absolute;
		content: '';
	}

	.list > li::after{
		top: 20px;
		bottom: 20px;
		left: 0px;
		border-left: 2px dashed #999;
		width: 2px;
	}
	.list > li::before{
		top: 0px;
		height: 2px;
		left: 17px;
		right: 17px;
		border-top: 2px dashed #999;
	}

	.img-block { height: 150px;}
		p { font-size: 17px; margin-bottom: 10px;}

		.price {
			color: #F00;
			font-size: 20px;
			font-weight: bold;
		}

А вот и сам результат.

Сразу предупрежу вас, что смотреть пример следует в браузере Opera 12.10+. Для того, чтобы не раздувать код лишними правилами, я использовал последний стандартизированный синтаксис Flexbox'ов, который на данный день (25.11.2012) поддерживается только в Opera. 

А теперь давайте разбираться.

Правило, которое идёт первым, касается обнуления стилей и изменения алгоритма расчёта ширины блочных элементов.

* {
        -moz-box-sizing: border-box;
	box-sizing: border-box;
	padding: 0;
	margin: 0;
}

Так как ширина наших пунктов выставлена в процентах, я решил добавить им в стили box-sizing: border-box, чтобы мы могли прописывать им любые границы или отступы, и при этом не боясь, что наша вёрстка поедет. Вообще, надо заметить, что само по себе box-sizing очень полезное свойство, которое, на мой взгляд, стоит применять в большинстве задач. Обязательно почитайте о нём.

Ну, а теперь включаем flexbox'ы:

.list {
	overflow: hidden;
	margin-left: 3px;
	margin-top: 2px;

	display: flex;
	flex-flow: row wrap;
}

Три верхних объявления мы разберём чуть позже, а вот двух последних коснёмся именно сейчас.

Строчкой display: flex мы говорим элементу <ul>, чтобы он стал  flexbox'ом. А объявление flex-flow: row wrap передаёт дочерним элементам (в нашем случае всем <li>), чтобы они шли в ряд и могли переноситься на следующую строку, если их суммарная ширина превысит родительскую. 

Далее идут сами пункты:

.list > li {
	position: relative;
	padding: 40px 30px;
	margin-top: -2px;
	margin-left: -3px;
	text-align: center;

	flex: 1 0 33.333%;
}

Как и в предыдущем случае, оставим несколько объявлений на потом и обсудим последнее.

Если контент пунктов не сможет вместиться в указанную ширину (одну треть), то они перенесутся на следующую строку, автоматически перестроившись сначала по два в ряд, а потом и вовсе по одному. При этом, благодаря первому параметру во flex (равносильному отдельному свойству flex-grow: 1), они увеличат свою ширину на равную долю оставшегося в строке свободного места. Второй параметр (эквивалентный flex-shrink: 0) гарантирует, что ширина боксов не станет меньше желаемой.

Кстати, если захотите, вы можете ограничить ширину пунктов. В этом примере я специально не стал этого делать, чтобы продемонстрировать более универсальное решение и мощь Flexbox'ов.

В дополнение ко всему, благодаря полезным свойствам Flexbox'a высота пунктов должна подстраиваться под высоту контейнера, что будет являться для нас несомненным плюсом. 

Пора переходить к границам:

	.list > li:after,
	.list > li:before{
		position: absolute;
		content: '';
	}

	.list > li:after{
		top: 20px;
		bottom: 20px;
		left: 0px;
		border-left: 2px dashed #999;
		width: 2px;
	}
	.list > li:before{
		top: 0px;
		height: 2px;
		left: 17px;
		right: 17px;
		border-top: 2px dashed #999;
	}

Я решил, что раз в нашем случае пункты беспроблемно растягиваются по ширине и высоте, то мы вполне можем воспользоваться этим, применив к ним пару псевдоэлементов. Как вы уже догадались, псевдоэлементы будут являться абсолютно позиционированными границами. :after будет отвечать за правую границу, а :before за верхнюю. С помощью нужных координат эти псевдограницы будут начинаться и заканчиваться там, где нам нужно. :after, например, в 20px от низа и верха. 

А теперь вспомним те объявления, которые мы растеряли по дороге. 

Так как левых границ не должно быть видно, мне пришлось немного отодвинуть пункты влево и вверх с помощью отрицательных margin'ов, чтобы та их часть, в которой находятся псевдограницы – ушла за пределы контейнера. А чтобы границы вообще пропали из виду, я и повесил на <ul> overflow: hidden, обрезав их полностью.

Вот такое вот собственно простое решение можно сотворить благодаря Flexbox'ам. 

Суровая действительность

Помечтали? Достаточно. А теперь вернёмся к суровой действительности и вспомним, что не все браузеры, под которые нам приходится делать вёрстку, поддерживают светлые технологии. Речь идёт об IE9 и ниже. Поэтому дальше мы разберём решения, которые более приближены к реальности.

Начнём с фиксированных решений, где пункты будут обладать точной шириной и высотой. 

Старые добрые float'ы

Неудивительно, что задумываясь о кроссбраузерности первым делом мне в голову пришла идея с float'ами. Это популярнейшее свойство на данный момент является самым известным по части раскладочных инструментов, хотя предназначено совсем для иных целей:) Но что поделать, будем использовать то что имеем.

Код, который будет повторяться, я писать не буду, дабы не плодить лишнего.  

Это решение основано на всё тех же псевдоэлементах и списке. Поэтому изменений особых нет, только лишь немного в CSS:

.list {
	width: 625px;
	overflow: hidden;
	margin: auto;

}
	.list > li {
		position: relative;
		padding: 40px 30px;
		margin-top: -2px;
		margin-left: -3px;
		text-align: center;

		width: 210px;
		height: 280px;
		float: left;
	}

Как можно заметить, я убрал все записи относяищеся к flexbox'ам и присвоил списку ширину, а пунктам задал общие размеры и float: left, чтобы они прижимались друг к другу. Вот что у нас получилось таким образом. Вполне себе ничего, правда? И работает везде, начиная с IE8:)

Таблица вместо списка

При необходимости, мы можем заменить список таблицей, но тогда нам придётся делать в ячейках лишний контейнер из-за бородатой проблемы с позиционированием в ячейках таблицы в Firefox.

Повторяющийся фон границ вместо псевдоэлементов

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

Повторяющийся CSS-фон

Что мы поменяли в коде на этот раз? Да собственно совсем немного:

.list {
	width: 630px;
	overflow: hidden;
	margin: auto;

	background: -40px -50px url(bg.gif);
}
	.list > li {
		width: 210px;
		height: 310px;
		padding: 40px 30px 0;
		text-align: center;
		float: left;

		margin-bottom: -6px;
	}

Из кода пропали псевдоэлементы, но зато появился фон и отрицательный margin. Фон я установил на элемент <ul>, а margin на элементы <li>. Я не стал заморачиваться с размерами пунктов, а просто настроил их так, чтобы их границы совпадали с повторяющимся фоном. Честно говоря, можно было бы сделать красивее, поигравшись с изображением в фотошопе и с кодом, но я оставлю это вам, тем более, что проект проекту рознь, а значит и изображения каждый будет подбирать под свои задачи. 

Честно говоря, это сомнительный вариант, т.к. требует повышенной чёткости в размерах пунктов и изображений. Но тем не менее выставляю его на суд, хотя бы в качестве пищи для размышления.

Универсальное решение для резиновых размеров

Как вы считаете, настало ли время повторить подвиг Flexbox'ов? Думаю, что да. Но сразу хочу предупредить, что, к сожалению, на данный день в арсенале CSS2.1 не существует решения сделать высоту наших пунктов автоматической и при этом подстраивающуюся под высоту самого высокого пункта в строке. Поэтому моё решение будет, во-первых, смахивать в сторону хардкора, а во-вторых, имеет ограничение, чуть позже вы поймёте, какое.

В HTML-коде произошли кардинальные изменения. Я убрал список, заменив его на <div>'ы, и теперь код HTML выглядит так:

	<div class="line"><div class="line__inner">
		<div class="item">
			<div class="img-block"><img src="img1.jpg" /></div>
			<p>Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, </p>
			<span class="price">$1270.99</span>
		</div>
		<div class="item">
			<div class="img-block"><img src="img2.jpg" /></div>
			<p>Toshiba</p>
			<span class="price">$870.99</span>
		</div>
		<div class="item">
			<div class="img-block"><img src="img3.jpg" /></div>
			<p>Asus</p>
			<span class="price">$60.99</span>
		</div>
	</div></div>
       .....

Это пример одного ряда. Теперь, для пунктов я использую пару контейнеров (.line и .line__inner). Мы вернёмся к этому моменту позже, т.к для начала стоит взглянуть на CSS, в котором тоже произошли изменения. 

* {
	-moz-box-sizing: border-box;

	box-sizing: border-box;
	padding: 0;
	margin: 0;
}
.line {
	width: 90%;
	overflow: hidden;
	margin: 20px auto;
	min-width: 650px;
}
	.line__inner {
		position: relative;
		padding: 20px 0;
		white-space: nowrap;
	}
	.item {
		width: 33.3333%;
		display: inline-block;
		position: relative;
		vertical-align: top;
		white-space: normal;
		text-align: center;
		padding: 0 30px;
		margin-left: -3px;
	}

	.line:first-child { margin-top: 0;}
	.line:first-child .item:after{ display: none; margin-top: 0;}

	.item:after,
	.item:before{
		position: absolute;
		content: '';
	}

	.item:after{
		top: -20px;
		height: 2px;
		left: 20px;
		border-top: 2px dashed #999;
		right: 20px
	}
	.item:before{
		top: 0px;
		height: 5000px;
		left: 0px;
		border-left: 2px dashed #999;
		width: 2px;
	}

А теперь давайте разбираться. В общем, я решил воспользоваться одним из старых трюков с эмуляцией колонок одинаковой высоты. Если убрать всё лишнее, то можно увидеть, в чём заключается это решение. Всё очень просто. Каждый псевдоэлемент (в нашем случае :before) находится в своём пункте и растягивается по высоте на то значение, которого точно хватило бы для колонки любой высоты. Так как псевдоэлемент абсолютно позиционирован, то его размеры никогда не растянут контейнер, потому что элемент отсутствует в потоке. Но при этом, благодаря огромной высоте, псевдоэлемент легко растягивается на всю высоту контейнера (а по факту превышает её), который в свою очередь вмещает все находящиеся в нём колонки, растягиваясь по высоте самой длинной. Таким образом с помощью overflow: hidden у родительского блока мы легко можем обрезать всё лишнее и привести все псевдоколонки к одинаковой высоте.

В нашем случае нет задачи сделать колонки одинаковой высоты. Но почему бы не заменить их на границы, использовав тот же приём? Я считаю это вполне допустимым, тем более что это работает.

Да, у этого способа есть и ограничение, о котором я писал выше. Пункты не должны переноситься на следующий ряд, т.к. в этом случае мы не сможем обрезать псевдоэлементы и задача не выполнится. Поэтому я заменил плавающий механизм на строчно-блочный (inline-block). Это было необходимо для перестраховки, чтобы снаружи я мог выставить родительскому контейнеру white-space: nowrap, зная, что при любой ширине все мои пункты не смогут случайно перескочить на следующую строку. Соответственно, у самих пунктов я вернул white-space в значение normal, чтобы элементы внутри них вели себя хорошо:)

В остальном, я думаю, из кода всё довольно очевидно. Единственное, на что стоит обратить внимание, это на дополнительный контейнер у родительского блока (.line__inner). Без него мне не удалось обойтись. Дело в том, что, т.к. высота псевдоэлемента обрезается самим контейнером, то скушать рыбку и не обляпаться добиться нужных отступов снизу и сверху не получится (upd: получиловь всё же. Спасибо gordi). Но с помощью дополнительного контейнера эта задача решается простыми padding'ами.

В общем, вот итоговый результат, смотрите сами.

Это решение прекрасно ведёт себя в любом браузере, начиная с IE8+, но если вам вдруг необходимо наладить его работу в IE7, то придётся заменить псевдоэлементы на… допустим <span>'ы, а inline-block'и на известные хаки.

Заключение

В данной статье, я продемонстрировал несколько решений этой задачи. Но я уверен, что это далеко не конец. Поэтому очень надеюсь, что в комментариях вы предложите и свои варианты. Самые интересные я обязательно включу в статью.

(27.11.2012) В комментариях, один добрый человек (gordi) улучшил последнее решение, избавившись от лишнего контейнера (.line__inner).  

P.S. Это тоже может быть интересно:

12 комментариев

  1.  Флексы это хорошо, но зачем костылять overflow:hidden?
    не лучше ли для современных браузеров использовать :nth-child? Тем более, что количество колонок фиксированное.

    1. Количество колонок фиксированное, но есть интересный эффект: если окно узкое, то колонок может стать одна или две. В данной реализации проблем с этим нет.

      1. Всё правильно. Плюс к этому я специально не стал применять :nth-child ещё и по той причине, что мне хотелось нарочно засветить этот трюк с отрицательными margin'ами. Кому-то, да пригодится:)

        В своих же задачах вы можете поступать, как хотите. 

  2. Без относительно к самому предложенному решению.
    Можно обойтись без вкладывания картинок в дополнительный контейнер с классом — img-block в пследнем примере.

    1. А это не играет роли и погоды не делает. Вот если бы ты предложил как-нить сделать такое же решение, но без  <div class="line__inner">, вот тогда было бы здорово:)

        1. Да, но оно никак не влияет на само решение. По сути, в сами пункты можно было бы запихнуть всё что угодно. Но я тебя понял, учту:)

  3. У меня вопрос, а зачем мы задаем width и height в 2px псевдоэлементам? Вроде поубирал эти свойства и ничего не изменилось.

    1. Я прописал эти свойства из-за какого-то косяка прошлого (некая перестраховочка). Но видимо, да, ни к чему они. 

Оставить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Получать новые комментарии по электронной почте. Вы можете подписаться без комментирования.