Розкладка в CSS: float

Довга епопея з написанням статті про властивість «float» завершена. Нехай часу пішло багато, але мені здається, що мені вдалося максимально дохідливо звести все різноманіття поводжень цієї потужної й трохи дивної властивості в одну загальну систему.

Доля властивості «float» в CSS злегка схожа на долю тега «table» в HTML: ні те, ні інше взагалі не замислювалося як засіб створення колонок і взагалі розкладки елементів. Однак через певні недосконалості механізму позиціонування, float використовується для цієї мети дуже широко. А те, що придумувався він для іншого, частенько виявляється різними неочевидними ефектами. Однак перед тим, як їх показати, я все ж розповім, як float можна застосовувати для розкладки.

На самому початку – невелике зауваження про терміни. У російській мові не склалося жодного відомого терміна для цього інструменту (поки, принаймні). Тому я вважаю за краще писати його у вихідному написанні – «float«. Читається це приблизно як «флоут» (ламати вилиці вимовою «флоАт» не потрібно). Водночас, тут же прошу пробачити мені такі вільності як «заfloat’ити», «приfloat’нутий» і т.п.

Принцип роботи

Як і позиціонування, float використовується для того, щоб рухати бокси. Але на відміну від позиціонування, яким можна рухати бокси практично довільно, все, що може float – це зрушити елемент до однієї з сторін потоку: правої або лівої.

При цьому сам бокс і наступні за ним в потоці набувають цікавої поведінки:

  1. float’нутий бокс зміщується по горизонталі і прилипає до однієї з сторін батька.
  2. float’нутий бокс перестає роздаватися на всю ширину батьківського боксу-контейнера (як це відбувається з блоками в потоці). З його непритиснутої до батьків вільної сторони з’являється вільне місце.
  3. Наступні за ним блокові бокси підтягуються вгору і займають його місце, як якщо б float’нутого боксу в потоці не було.
  4. Рядкові ж бокси всередині підсунувшись наверх блоків починають обтікати float’нутий бокс з вільної сторони.

Хочу ще раз підкреслити очевидну відразу річ: сама коробка блоку, наступного за float’ом, підлазить під нього і займає всю ширину потоку, а от текст всередині цього блоку зміщується в бік і обтікає float.

Далі цікаво, як себе ведуть float’нуті в одну сторону бокси, які йдуть один за іншим. У цьому випадку наступний бокс буде намагатися вміститися збоку від попереднього, з його вільної сторони. І тільки якщо йому там не буде достатньо місця, тоді він зміститься нижче і буде намагатися вміститися вже там.

Є ще один маленький технічний аспект, не обов’язковий для розуміння всієї «механіки». Заfloat’ити можна як блокові бокси, так і малі. При цьому рядкові відразу автоматично стають блоковими. Тобто, писати display: block; для float’а зайве.

З двох описаних особливостей float’ів – притискання до краю і стиснення збоку один одного – випливають два основні застосування їх у розкладці:

  • поділ сторінки на колонки
  • горизонтально розташовані меню

Колонки

Колонки – це коли блоки тексту розташовані поруч один з одним і мають однакову висоту.

Всі колоночні розкладки я буду розглядати на ось такому простому HTML коді з двома блоками:

<body>
  <div id="sidebar">
    ...
  </div>
  <div id="content">
    ...
  </div>
</body>

Відразу варто сказати, що робити колонки в контейнері, який розтягується по ширині, складніше, ніж із заданою шириною. Тут є два принципово різних підходи, що підходять для різних випадків.

Пропорційна ширина

Якщо потрібно, щоб ширина колонок змінювалася пропорційно, при зміні ширини сторінки, то підхід такий: два блоки float’яться поруч у різні боки, а їх ширина ділиться у потрібному процентному співвідношенні. Цей спосіб дозволяє легко поміняти колонки місцями – просто помінявши значення right і left. Тобто:

#content {
  float: right; width: 70%;
}

#sidebar {
  float: left; width: 30%;
}

Розтягування тільки однієї колонки

Якщо потрібно, щоб мінялася тільки ширина основної колонки, то попередній спосіб не підходить. Справа в тому, що в CSS, на жаль, не можна напряму сформулювати таку річ як «вся доступна ширина мінус конкретне число».

Тепер звернемося до однієї з попередніх статей про блоки в потоці, де я згадав про одну їх корисну особливість – автоматично укладатися по ширині в розмір батьківського боксу. Тобто, якщо блоку в прямому потоці задати, скажімо, лівий margin, то його ширина відповідно стиснеться. А це саме та поведінка, якої ми хотіли домогтися від однієї з колонок.

Отже, для потрібного нам ефекту ми дамо основному блоку лівий margin, щоб він стиснувся направо, а бічну панель заfloat’имо на це місце:

#sidebar {
  float:left; width:200px;
}

#content {
  margin-left:200px;
}

Але у другого способу є один дуже серйозний недолік. Зверніть увагу, що в початковому HTML блок «sidebar» йде до блоку «content» з основним вмістом. Не потрібно думати, що так зроблено випадково. Так зроблено спеціально, тому що інакше цей самий другий спосіб з накладенням колонки поверх margin не працював би.

Як я написав на початку статті, float’и зсуваються тільки убік і дають місце таким блоками, які з’їжджають наверх. Тому принципово, щоб «sidebar» був вже нагорі, і тоді основний блок під’їде до нього. Якщо «sidebar» йде після основного блоку, то він так і залишиться нижче, і ні на які колонки це схоже не буде.

Це дійсно погано, тому що перекреслює одну з основних ідей CSS: відділення оформлення від змісту. Виходить, що ми захотіли змінити тільки дизайн, а якщо блоки розташовані «не так», то доведеться лізти ще й в HTML-шаблони. Крім того, з точки зору структури можуть бути свої вагомі підстави розташовувати блоки так, а не інакше. Наприклад щоб користувач міг почати читати основний текст сторінки, не чекаючи завантаження навігації.

Фіксована ширина

Все різко спрощується, коли колонки поміщаються у фіксовану ширину контейнера. У цьому випадку краще всього використовувати перший спосіб (float’ити всі колонки), і ширину вже можна ставити не тільки у відсотках, але й в чому хочете, оскільки її можна точно обчислити.

Висота колонок

Знову-таки, я далеко не випадково «відрізав» на картинках нижню частину блоків. Інакше б вони як колонки зовсім не виглядали, тому що, як неважко переконатися, якщо застосувати ті фрагменти CSS, що я привів, і розфарбувати колонки різними кольорами, то їх висота виявляється різною. Вона залежить від кількості вмісту в цих блоках.

Цей негарний ефект можна обійти кількома способами.

Перший спосіб називається «Помилкові колонки» («Faux columns»), опублікований в авторитетному веб-журналі A List Apart у вересні 2004 року і з тих пір користується великою популярністю. Всім рекомендую прочитати або оригінал, або російський переклад. Однак, якщо ви сьогодні не в настрої клікати, то ось коротко його сутність.

Замість того, щоб призначати фон самим колонкам, вони залишаються прозорими, а от їх контейнеру призначається фонова картинка шириною у весь контейнер і повторювана по вертикалі. Частини цієї картинки, що знаходяться під різними колонками, фарбуються в різні кольори і при повторенні вниз це дає потрібний візуальний ефект.

Що в помилкових колонках чудово – так це те, що ви можете не обмежувати себе на фоновій картинці рівними кольорами. На ній, наприклад, можна намалювати ефект тіні між колонками, горизонтальні смужки у вигляді фону, які повторюються, орнамент по краях.

Недолік способу полягає в тому, що оскільки у фонової картинки є тільки один розмір, її не можна застосовувати для колонок, що пропорційно розтягуються, так як картинка тягнутися не буде (тут я помилився; все цілком можна застосувати, читайте коментар). А ось для випадку, коли одна з колонок фіксована по ширині, фон пристосувати можна (цей випадок, до речі, в «Faux columns» не розглянуто).

Сутність полягає в тому, щоб помістити фонову картинку тільки під ту колонку, ширина якої відома. Решта місць буде зайнята фоновим кольором контейнера, а не картинкою.

Візьмемо наш приклад і зробимо колонку «sidebar» праворуч шириною 200 пікселів, а «content» нехай розтягується. Для «sidebar» підготуємо картинку розмірами 200х1 наприклад рівного синього відтінку. А під «content» відведемо жовтуватий.

У стилях це виглядає так:

#sidebar {
  float:right; width:200px;
}

#content {
  margin-right:200px;
}

body {
  background:url(bg.png) #FFD right top repeat-y;
}

Єдине правило для контейнера (body) задає всю поведінку фону:

  • Вказується URL малюнка (bg.png)
  • Колір фону в тих місцях, де його не буде (#FFD)
  • Положення малюнка притиснутого до правого краю (right top)
  • Повторення картинки вниз (repeat-y)

У реальному прикладі в CSS ще була пара правил для кольору літер і боротьби з кордонами, які зараз не істотні.

Інший спосіб зрівняння колонок по висоті був описаний недавно і вже сильно відомий, оскільки зручний.

Ідея його полягає в тому, щоб нерівність висот колонок заховати, неймовірно подовживши їх вниз, щоб їх кінці були за межами реального вмісту сторінки.

Домагаються цього тим, що спочатку ставлять колонкам дуже великий відступ (padding) вниз, який зафарбовується кольором фону.

А щоб весь інший вміст теж не зміщувався туди далеко, колонкам призначається негативний кордон (margin) такого ж розміру:

#content,
#sidebar {
  padding-bottom:32767px;
  margin-bottom:-32767px;
}

Дивне число обумовлено тим, що це максимум, який може дозволити браузер Safari. Насправді воно не настільки дивне. Кому цікаво, це максимальне знакове ціле число, якщо уявляти його 16 бітами.

У результаті все, що йде за колонками, зміщується і знаходиться прямо під вмістом найдовшої з них, а колонки подовжуються вниз. Негарно одне – із-за довгих колонок сама сторінка стає такою ж довгою. Щоб з цим боротися, треба їх контейнеру проставити властивість overflow: hidden, яка змушує контейнер просто відрізати і не показувати те, що виходить за його межі.

У мого прикладу, який я взяв на самому початку, є, правда, одна проблема. Там колонки лежать прямо в body. А якщо body проставити overflow: hidden, то браузери скасовують скролінг у сторінки начисто. Навіть якщо реальний вміст вище вікна. Тому колонки треба загорнути у ще один елемент, наприклад div. Але справедливості заради треба сказати, що на практиці колонки так буває вже у що-небудь загорнуті.

Засідка

Куди ж без неї. Як я не особливо прозоро натякнув на самому початку, оскільки float’и не придумували як засіб створення колонок, це обов’язково вилізе чим-небудь потворним і відгукнеться збільшенням витрати анальгіну (деякі вважають за краще темпалгін або парацетамол).

Причому «вилізе» – у прямому сенсі. Давайте трохи посунемо наш «голий» приклад у бік реальності, додавши над колонками шапку і внизу який-небудь теж блок з текстом.

<body>
  <div id="header">...</div>

  <div id="main">
    <div id="content">
    ...
    </div>

    <div id="sidebar">
    ...
    </div>
  </div>

  <div id="footer">...</div>
</body>

Для простоти виберемо нехитрий стовпчиковий дизайн з фіксованою шириною. Шапку та нижній блок зробимо синіми з білими літерами, основний вміст білим, а додаткову колонку теж синьою, але трохи світлішою. Кольори колонкам задамо способом «Faux columns».

/* Розкладка в колонки */

body {
  width:600px;
  margin:0 auto;
}

#content {
  float:left; width:450px;
}

#sidebar {
  float:right; width:150px;
}

/* Кольори */

#main {
  background:url(bg.png) right top repeat-y;
}

#header,
#footer {
  background:#238; color:white;
}

Всякі відступи і шрифти я знову опустив для простоти сприйняття. Додамо тестового тексту і запускаємо:

Хм … Ну, колонки, загалом, навіть можна розгледіти! Не причепишся! Утім, як не крути, але виглядає все не так, як задумано, а навіть можна сказати, все перетворилося на якусь кашу з кольорів і букв.

Щоб зрозуміти, чому так відбувається, треба згадати опис того, як працює float. А саме, що блоки, які йдуть за float’ами, перестають ці float’и помічати і підтягуються вгору. Більше того, сам контейнер, в якому float’и знаходяться, теж перестає їх помічати і float’нуті блоки провалюються через низ контейнера, якщо він закінчується раніше.

Тепер подивимося на наш код. Обидва float’нуті блоки «content» і «sidebar» знаходяться всередині блоку «main». І більше нічого в «main» немає. А раз йому нічого більше утримувати, то його висота сплющується в нуль! Тому й не видно на картинці ні білого фону «content», ні світло-синього фону «sidebar», тому що ці кольори призначені у вигляді фону «main».

Далі – «footer». Він, підкоряючись все тому ж правилу, теж не бачить float’нутих блоків і підтягується вгору прямо до самого заголовку (оскільки «main» – нульової висоти). Але в «footer» є текст. Текст цей вже повинен обтікати float’и: справа «content» і зліва «sidebar». Між колонками місця не залишилося, тому текст може початися тільки під однією з колонок, яка перша скінчиться. Там він і є. Таким чином, «footer», підтягнувшись під заголовок, продовжується вниз, поки не закінчиться весь його текст. І весь цей синій фон, що нижче заголовка – це «footer» і є.

Навіщо така складність

Описана поведінка має вселяти подив. Навіщо треба було вигадувати такі складнощі: розділити поняття блоку так, щоб кольори і рамки наверх, а текст – на місці? Але сенс, звичайно, є. Це, поряд зі схлопуванням кордонів, спроба змусити боксову модель CSS нормально вести себе в умовах простого потоку тексту. Детальне класичне пояснення цьому феномену є все у того ж Еріка Мейєра в статті «Containing Floats» (англійською). Постараюся коротко передати суть.

Уявіть собі звичайний потік абзаців – блоків з текстом – без всякого позиціонування. В одному з абзаців зустрічається картинка, яку хочеться зрушити, скажімо, вліво, щоб текст її обтікав. Таке раніше в HTML досягалося властивістю align = "left", але в дусі винесення оформлення з HTML у стилі, для цієї функції саме і придумали властивість float. Тобто замість align цій картинці приписується float: left.

Нехай картинка, що зсувається, займає по висоті більше, ніж текст абзацу. Якщо вона буде розтягувати свій абзац і відсувати початок наступного нижче, то це буде виглядати непривабливо через збільшення відстані між рядками сусідніх абзаців. Тому вона і провалюється через усі межі блоків, зберігаючи між ними гарні відступи, а текст її обтікає.

Рішення

Отже, поведінка з проваленням зрозуміла, але вона зручна для непозиційованого тексту, а для розкладки – зовсім незручна. Існує два підходи, які усувають обидва прояви цієї властивості: підтягування наступних боксів наверх і провалювання через низ контейнера.

Для усунення підтягування блоків існує спеціальна властивість – clear. Вона змушує елемент зсуватися вниз, поки збоку від нього не залишиться float’ів. Причому, можна керувати, з якого саме боку не повинно бути float’ів:

clear: left
стежить, щоб float’ів не було зліва
clear: right
стежить, щоб float’ів не було праворуч
clear: both
стежить, щоб float’ів не було з обох сторін

Таким чином, якщо ми скажемо нашому «footer»-у:

#footer {
  clear:both;
}

… щоб ліворуч і праворуч від нього не було float’нутих колонок, то він зрушиться вниз як раз туди, де вони обидві закінчуються.

Але це не вирішує іншої проблеми: того, що float’и провалюються через «main», і той сплющується, в результаті чого не видно стовпчик фону, який йому призначено. Провалювання можна перемогти двома способами.

Можна явно спозиціонувати контейнер яким-небудь чином. Наскільки я розумію логіку специфікації, поведінка провалювання вважається логічною тільки в простому потоці. В інших випадках вона тільки заважає. І так воно і є, як ми переконалися. Тобто, досить призначити контейнеру, наприклад, position: absolute або float: left, і нічого не буде провалювитися, контейнер буде повністю укладати в себе і текст, і float’и. У нашому випадку (і в більшості випадків, до речі) це рішення цілком підійде.

Інший цікавий спосіб пов’язаний з побічним ефектом властивості overflow. Сама по собі вона призначена для того, щоб визначати, як буде вести себе контейнер при переповненні, коли не може вмістити свій вміст. У нього є чотири значення:

visible
вміст переходить через край і його нормально видно (це поведінка за замовчуванням)
hidden
вміст відсікається за межами контейнера та його там ніяк не видно
auto
якщо вміст переповнює контейнер, у нього з’являється скроллбар, що дозволяє прокручувати вміст, якщо ні – не з’являється
scroll
схоже на auto, тільки скроллбар у контейнера є завжди, навіть коли вміст його не переповнює

Так ось, побічний ефект полягає в тому, що якщо контейнеру поставити будь-який overflow, крім звичайного visible, він раптом розтягується і містить в собі float’и, які в ньому сидять, усуваючи провалювання.

Який же overflow використовувати? Відразу відпадає scroll – скроллбари, які завжди висять, явно не потрібні. Залишаються auto і hidden, які відрізняються тільки тим, з’являється скроллбар при переповненні чи ні. Але у нас ніякого переповнення немає, навпаки, цією властивістю ми змусили контейнер додатково розтягнутися, щоб він охоплював всі свої елементи. Тому використовувати можна будь-яке значення.

Я забобонно намагаюся використовувати hidden, щоб не з’являлося скроллбарів, якщо через якісь глюки переповнення раптом виникне.

У рішення з overflow є одна проблема, пов’язана з поведінкою Деякого Браузера™. Воно працює тільки якщо контейнеру явно призначені ширина або висота. Через це їм іноді незручно користуватися, коли вам потрібні автоматичні розміри, а не жорсткі.

Отже, в підсумку, щоб виправити наш приклад з колонками, треба зробити так:

#main {
  width:100%; overflow:hidden;
}

До речі ! Якби для малювання фону під колонками я використав не faux columns, а спосіб з довгим padding’ом, то він би зажадав використовувати overflow: hidden для «main», що заразом вирішує і проблему з проваленням. Але як би тоді я про це розповідав?

Все на цьому з колонками … Час сходити налити собі смачного чаю (багато хто воліє кави) і, додавши до цього якусь смачну булочку, зробити паузу для укладання в голові всього цього місива. Далі нас чекає маленька добавка – про меню.

Меню

Нагадаю, що якщо кілька блоків, які йдуть підряд, заfloat’ити в один бік, то кожен наступний буде намагатися розкластися з вільного боку від попереднього. Цей ефект широко використовується для того, щоб перетворювати списки розділів сайту в горизонтально розташовані меню.

Візьмемо такий список:

<ul>
  <li><a href="/">Початок</a></li>
  <li><a href="catalog/">Каталог</a></li>
  <li><a href="basket/">Кошик</a></li>
  <li><a href="help/">Довідка</a></li>
</ul>

Щоб це було схоже на меню, треба заfloat’ити всі li вліво, прибрати у них атрибутику списку (відступи і буліти) і ще додати для краси відступи, фон і рамку:

/* розкладка */

ul,
li {
  float:left;
  list-style:none;
  margin:0; padding:0;
}

/* вид */

li {
  padding:2px 10px;
  font:Bold Small Tahoma;
  background:#35C; color:white;
  border:solid 1px; border-color:#46F #238 #238 #46F;
}

a {
  color:white; text-decoration:none;
}

Зверніть увагу, що для розкладки всі властивості призначаються і для елементів ul, і для li. Це зручно звести в одне правило, тому що:

  • float: left потрібен елементам списку, щоб вони розклалися горизонтально, а самому списку – щоб елементи через нього не провалювалися;
  • Нульові margin і padding усувають відступи, які браузери роблять для списків за замовчуванням, але вони це роблять дуже по-різному, тому простіше сказати «всім все по нулях», ніж пам’ятати, що окремо для якого елементу проставляти.

Мораль

Механізм float – ще один засіб розкладки поряд з абсолютним позиціонуванням.

Він відрізняється в кращий бік, дозволяючи залишити елемент в потоці, що дуже зручно для колоночної розкладки.

Проте, його мінусами є менша гнучкість через те, що елементи не можна рухати довільно, і тому що можливість його застосовувати може залежати від порядку елементів в HTML-розмітці.

***

Це переклад статті. Оригінал тут