Плавающие элементы и обтекание текстом в CSS

Чтение — около 9 минут· без учёта практики

В предыдущем уроке мы закончили с позиционированием. Плавающие элементы — ещё один способ сдвинуть элемент с его места в потоке. Когда-то на них строили целые страницы: колонки, меню и сетки. Для раскладки страниц сейчас есть Flexbox и Grid, а у float осталась задача, ради которой его и придумали, — обтекание фото текстом, как в журнале. Этот урок дополнительный: в проект сайта Тони он не входит, но без него не прочитать старый CSS, а такой код встречается часто.

Свойство float

Пусть нужно компактно разместить на странице заголовок, два фото и текст. В примере ниже за каждым фото идёт отдельный абзац. Без float каждое фото займёт отдельную строку, а рядом с ним останется много пустого места. Чтобы текст встал рядом с фото, используют свойство float. Оно прижимает элемент к левому или правому краю, и элемент становится плавающим: содержимое, которое идёт после него, поднимается вверх и обтекает его с другой стороны.

float: попробуйте сами

Первое фото прижато к левому краю, и абзац обтекает его справа; второе — к правому, и текст обтекает его слева. Удалите float у класса .float-left — фото встанет на отдельную строку над абзацем.

index.html
HTML
<!doctype html>
<html lang="ru">
  <head>
    <meta charset="utf-8">
    <title>Обтекание фото текстом: float</title>
    <style>
      body { font-family: system-ui, sans-serif; background-color: linen; color: #2b211b; line-height: 1.6; }
      h2 {
        text-align: center;
      }
      img {
        width: 30%; /* 30% ширины родителя */
        height: auto;
      }
      .float-left {
        float: left; /* фото прижато к левому краю, текст обтекает его справа */
        margin: 0 1rem 0.5rem 0; /* отступ между фото и текстом */
      }
      .float-right {
        float: right; /* фото прижато к правому краю, текст обтекает его слева */
        margin: 0 0 0.5rem 1rem;
      }
    </style>
  </head>
  <body>
    <h2>Тони</h2>
    <img src="assets/tony-toy.png" alt="Тони с канатом в зубах" class="float-left" width="300" height="400">
    <p>
      Тони — карликовый пинчер чёрно-подпалого окраса. Он весит чуть больше четырёх килограммов, но уверен, что охраняет
      весь дом. Больше всего на свете Тони любит канат с мячиком «Удав»: его можно бросать, тянуть и приносить обратно
      сколько угодно раз.
    </p>
    <img src="assets/album-hoodie.png" alt="Тони в бежевой толстовке с капюшоном" class="float-right" width="300" height="400">
    <p>
      Короткая шерсть пинчера плохо греет, поэтому в холодную погоду Тони гуляет в толстовке. На прогулке он внимательно
      изучает каждый куст у пруда, здоровается с соседскими собаками и не упускает случая погонять голубей.
    </p>
  </body>
</html>

Что мы сделали в этом примере:

  • Задали изображениям ширину 30% от родителя, а заголовок выровняли по центру.
  • В классе .float-left указали float: left: фото прижато к левому краю, а текст обтекает его справа. Чтобы текст не прилипал к фото, задали внешний отступ справа и снизу: margin: 0 1rem 0.5rem 0.
  • В классе .float-right — float: right: фото у правого края, текст слева, поэтому внешний отступ задан слева.

Расстояние до текста раньше часто задавали внутренним отступом изображения (padding). Это тоже работает, но внешний отступ удобнее: он не увеличивает само изображение, а у фото с рамкой или фоном не появляется лишнего поля.

У свойства float такие значения:

Значение Описание
none Элемент не плавающий и стоит на своём месте в потоке. Значение по умолчанию.
left Элемент прижат к левому краю, содержимое обтекает его справа.
right Элемент прижат к правому краю, содержимое обтекает его слева.
inline-start, inline-end Логические значения: элемент прижат к началу или концу строки. В русском тексте это левый и правый край, а в языках с письмом справа налево, например в арабском, — наоборот.

Плавающий элемент внутри контейнера

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

Плавающий элемент в контейнере: попробуйте сами

Контейнер шириной 60% стоит по центру, а плавающий блок прижат к его правому краю. Обратите внимание: блок шириной по содержимому, хотя это div.

index.html
HTML
<!doctype html>
<html lang="ru">
  <head>
    <meta charset="utf-8">
    <title>Плавающий элемент внутри контейнера</title>
    <style>
      body { font-family: system-ui, sans-serif; background-color: linen; color: #2b211b; line-height: 1.6; }
      .container {
        width: 60%; /* 60% ширины родителя */
        height: 200px;
        margin: 0 auto; /* контейнер по центру */
        background-color: tan;
      }
      .float-right {
        float: right; /* прижат к правому краю контейнера, а не окна */
        border: 1px solid saddlebrown;
        background-color: #fffaf3;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <div class="float-right">
        <p>Что Тони любит больше?</p>
        <ul>
          <li>Мячик</li>
          <li>Канат</li>
          <li>Косточку</li>
        </ul>
      </div>
    </div>
  </body>
</html>

В этом примере контейнер шириной 60% от родителя (<body>) выровнен по центру и залит цветом tan. Внутри — блок <div> с float: right. Заметьте ещё одну особенность: внешнее поведение плавающего элемента становится блочным. При автоматической ширине обычный незамещаемый блок подбирает её по содержимому с учётом доступного места; длинный текст может переноситься. Внутренняя раскладка, например display: flex, при этом сохраняется.

Контейнер и плавающее фото

Браузер окружает плавающий элемент только текстом и строчным содержимым. Фон и рамки соседних блоков он не обтекает: они проходят под плавающим элементом. Кроме того, плавающий элемент исключается из нормального потока, и высота контейнера рассчитывается без него. Если фото выше текста, контейнер заканчивается раньше фото, и оно «вываливается» вниз — поверх рамки и того, что идёт следом.

overflow и flow-root: попробуйте сами

В первой заметке рамка и фон заканчиваются после строки текста, а фото выходит за них. Во второй и третьей контейнер растянулся до низа фото: у одной overflow: auto, у другой display: flow-root.

index.html
HTML
<!doctype html>
<html lang="ru">
  <head>
    <meta charset="utf-8">
    <title>Контейнер с плавающим фото: overflow и flow-root</title>
    <style>
      body { font-family: system-ui, sans-serif; background-color: linen; color: #2b211b; line-height: 1.6; }
      .note {
        border: 3px solid seagreen;
        background-color: rgb(46 139 87 / 0.2);
        margin-bottom: 5rem;
      }
      img {
        float: right;
        width: 30%;
        height: auto;
      }
      .fix-overflow {
        overflow: auto; /* старый способ: контейнер включает плавающее фото */
      }
      .fix-flow-root {
        display: flow-root; /* современный способ без побочных эффектов */
      }
    </style>
  </head>
  <body>
    <div class="note">
      <img src="assets/album-squirrel.png" alt="Тони с плюшевой белкой" width="300" height="400">
      Без исправления: рамка и фон заканчиваются после текста, а фото выходит за контейнер.
    </div>
    <div class="note fix-overflow">
      <img src="assets/album-squirrel.png" alt="Тони с плюшевой белкой" width="300" height="400">
      overflow: auto — контейнер растянулся до низа фото.
    </div>
    <div class="note fix-flow-root">
      <img src="assets/album-squirrel.png" alt="Тони с плюшевой белкой" width="300" height="400">
      display: flow-root — тот же результат.
    </div>
  </body>
</html>

Чтобы контейнер включил плавающий элемент, нужно создать для него новый блочный контекст форматирования (англ. block formatting context, BFC) — независимую область, плавающие элементы которой не выходят наружу. Способов несколько:

  • display: flow-root — современный способ. Значение только и делает, что создаёт новый контекст: контейнер остаётся обычным блоком, без обрезки и полос прокрутки. При этом отступы его дочерних блоков больше не схлопываются с его отступами.
  • overflow: auto или overflow: hidden — способ, который вы встретите в старом коде. Свойство overflow мы рассматривали в уроке «Размеры элементов». Любое значение, кроме visible и clip, заставляет браузер пересчитать высоту блока так, чтобы она включала плавающий элемент. Но побочные эффекты остаются: hidden обрежет всё, что выходит за блок (например, тень или выпадающее меню), а auto может добавить полосу прокрутки.
  • Приём clearfix — тоже из старого кода. Контейнеру добавляют псевдоэлемент, который встаёт после всех плавающих элементов и отменяет обтекание:
CSS
/* clearfix: так контейнеры включали плавающие элементы до появления flow-root */
.clearfix::after {
  content: "";
  display: table;
  clear: both; /* о свойстве clear — в следующем разделе */
}

В новом коде используйте display: flow-root. А встретив overflow: hidden или класс clearfix рядом с плавающими элементами, вы теперь знаете, зачем они там.

Отмена обтекания: clear

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

Чтобы это исправить, используют свойство clear. Оно определяет, с какой стороны элемента не допускаются предшествующие плавающие элементы: если они там есть, элемент опускается ниже них. Учитываются float в том же блочном контексте форматирования; например, плавающее фото внутри отдельного flow-root не влияет на clear снаружи.

Значение Описание
none Плавающие элементы разрешены с обеих сторон. Значение по умолчанию.
left Плавающие элементы не разрешены с левой стороны.
right Плавающие элементы не разрешены с правой стороны.
both Плавающие элементы не разрешены ни с левой, ни с правой стороны.
inline-start, inline-end Логические значения: не разрешены со стороны начала или конца строки.

clear: попробуйте сами

У блоков clear: right, поэтому второй блок начинается под первым фото. Удалите clear — второй блок поднимется и начнёт обтекать первое фото, а фото выстроятся лесенкой.

index.html
HTML
<!doctype html>
<html lang="ru">
  <head>
    <meta charset="utf-8">
    <title>Отмена обтекания: clear</title>
    <style>
      body { font-family: system-ui, sans-serif; background-color: linen; color: #2b211b; line-height: 1.6; }
      div {
        border-top: 1px solid saddlebrown;
        background-color: rgb(139 69 19 / 0.15);
        clear: right; /* справа от блока не должно быть плавающих элементов */
      }
      img {
        width: 25%;
        height: auto;
      }
      .float-right {
        float: right;
        margin-bottom: 15px;
      }
    </style>
  </head>
  <body>
    <div>
      <img src="assets/album-puppy.png" alt="Тони-щенок" class="float-right" width="300" height="400">
      Я в детстве: смотрю в камеру и ещё не знаю, что такое канат.
    </div>
    <div>
      <img src="assets/album-laugh.png" alt="Тони смеётся, запрокинув голову" class="float-right" width="300" height="400">
      Смеюсь над шуткой хозяина.
    </div>
  </body>
</html>

В этом примере мы запретили блокам <div> плавающие элементы с правой стороны — отменили правила обтекания, и блоки выстроились один за другим. А чтобы отделить фото от следующего блока, задали изображениям внешний отступ снизу 15px.

Обтекание по форме: shape-outside

Плавающий элемент — прямоугольник, и текст обтекает именно его, даже если фото скруглено до круга. Свойство shape-outside задаёт плавающему элементу другую форму для обтекания: круг circle(), эллипс ellipse(), многоугольник polygon() или форму непрозрачной части изображения url(). Расстояние от формы до текста задаёт shape-margin.

shape-outside: попробуйте сами

Фото скруглено через border-radius: 50%, а shape-outside: circle(50%) заставляет текст обтекать его по кругу. Замените shape-margin: 1rem на 2rem — текст отодвинется от круга.

index.html
HTML
<!doctype html>
<html lang="ru">
  <head>
    <meta charset="utf-8">
    <title>Обтекание по форме: shape-outside</title>
    <style>
      body { font-family: system-ui, sans-serif; background-color: linen; color: #2b211b; line-height: 1.6; }
      img {
        float: left;
        width: 160px;
        height: 160px;
        object-fit: cover;
        border-radius: 50%; /* фото выглядит кругом */
        shape-outside: circle(50%); /* и текст обтекает его по кругу */
        shape-margin: 1rem; /* расстояние от круга до текста */
      }
    </style>
  </head>
  <body>
    <img src="assets/walk.jpg" alt="Тони на дорожке у пруда" width="240" height="426">
    <p>
      Шлейка надета — значит, идём гулять! Тони знает это лучше всех: стоит хозяину взять в руки оранжевую шлейку, как он
      уже сидит у двери. На прогулке он проверяет любимые кусты у пруда, здоровается с соседями и обязательно находит
      палку, которую нужно принести домой. Удалите свойство shape-outside — строки снова будут обтекать квадрат, в который
      вписан круг, и у краёв фото появятся пустые углы.
    </p>
  </body>
</html>

Свойство действует только на плавающие элементы. border-radius меняет только то, как фото выглядит, а shape-outside — только то, как его обтекает текст. Поэтому их задают вместе.

Когда float не нужен

Для раскладки страниц — колонок, меню в строку, сеток карточек — используйте Flexbox и Grid. Они не требуют отменять обтекание, выравнивают элементы по высоте и не заставляют контейнер «терять» содержимое. Плавающие элементы оставьте для обтекания текстом.

  • У флекс- и грид-элементов float не действует: если родитель — флекс- или грид-контейнер, свойство игнорируется.
  • Элемент с абсолютным или фиксированным позиционированием не может быть плавающим: float у него вычисляется в none. Об этом шла речь в первой части урока о позиционировании.
  • На узком экране фото с обтеканием может оставить для текста слишком узкую колонку. Задавайте фото ширину в процентах или отменяйте float медиазапросом.

Проверьте себя

Выберите ответ — объяснение появится сразу. Ответ можно менять.

1. Фото задали float: left. Что произойдёт с абзацем, который идёт после него?
Показать ответ

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

2. Блок <div> с float: right лежит в контейнере шириной 60%, выровненном по центру. Где окажется блок?
Показать ответ

У правого края контейнера — Плавающий элемент прижимается к краю своего родителя, а не окна.

3. Фото с float: right выше текста карточки, и рамка карточки заканчивается раньше фото. Как это исправить в новом коде?
Показать ответ

Задать карточке display: flow-root — Значение создаёт новый блочный контекст форматирования, и высота карточки включает плавающее фото без обрезки и полос прокрутки.

4. В старом CSS у контейнера с плавающими пунктами меню стоит overflow: hidden. Зачем?
Показать ответ

Чтобы контейнер включил плавающие пункты, а его фон не схлопнулся — Любое значение overflow, кроме visible и clip, пересчитывает высоту блока. Побочный эффект — обрезка всего, что выходит за блок.

5. Что делает правило footer { clear: both; } на странице со столбцами на float?
Показать ответ

Опускает подвал ниже предшествующих плавающих элементов с обеих сторон в том же блочном контексте форматирования — Clear учитывает предшествующие float в том же BFC. Плавающие элементы внутри отдельного flow-root на подвал снаружи не влияют.

6. Фото скруглили до круга через border-radius: 50%, но текст обтекает его по квадрату. Чего не хватает?
Показать ответ

shape-outside: circle(50%) — Border-radius меняет только вид фото, а форму обтекания задаёт shape-outside.

7. Флекс-элементу задали float: left. Что произойдёт?
Показать ответ

Ничего: у флекс-элементов float не действует — Если родитель — флекс- или грид-контейнер, свойство float игнорируется.

8. Четырём пунктам меню «Домой», «Раздел 1», «Раздел 2», «Раздел 3» задали float: right. В каком порядке они встанут слева направо?
Показать ответ

«Раздел 3», «Раздел 2», «Раздел 1», «Домой» — Первый пункт прижимается к правому краю, следующий — левее него, и так далее: порядок получается обратным.

Задание

Ожидаемый результат: фото Тони у правого края карточки, первый абзац обтекает его слева. Карточка со светлым фоном и рамкой заканчивается ниже фото, а строка «Следующая прогулка — завтра утром» стоит под карточкой. Уже 30rem (480 пикселей) фото стоит по центру над текстом, и текст занимает всю ширину карточки.

Выполните задание

index.html
HTML
<!doctype html>
<html lang="ru">
  <head>
    <meta charset="utf-8">
    <title>Заметка о прогулке</title>
    <style>
      body { font-family: system-ui, sans-serif; background-color: linen; color: #2b211b; line-height: 1.6; margin: 1rem; }
      .note {
        padding: 1rem;
        border: 1px solid tan;
        border-radius: 16px;
        background-color: #fffaf3;
      }
      .note img {
        width: 40%;
        height: auto;
        border-radius: 8px;
      }
    </style>
  </head>
  <body>
    <article class="note">
      <h2>Прогулка у пруда</h2>
      <img src="assets/walk.jpg" alt="Тони на дорожке у пруда" width="240" height="426">
      <p>Сегодня мы с хозяином гуляли у пруда целый час. Я проверил все кусты, поздоровался с таксой Бусей и нашёл
        отличную палку.</p>
      <h3>Что взять с собой</h3>
      <ul>
        <li>шлейку и поводок;</li>
        <li>канат с мячиком;</li>
        <li>воду и миску.</li>
      </ul>
    </article>
    <p>Следующая прогулка — завтра утром.</p>
  </body>
</html>

Дополнительная задача

Эта задача — тренировка чтения старого кода: в новых проектах такую страницу собирают на Grid. Сверстайте страницу на плавающих элементах:

  1. Шапка с меню из четырёх пунктов, прижатых к правому краю. Обратите внимание на порядок пунктов в результате: первым у правого края оказывается «Домой».
  2. Тёмная боковая панель слева: ширина 15% от родителя, высота 400px.
  3. Справа от панели — серое меню с пунктами в строку, под ним белая область с заголовком по центру.
  4. Под всем — серый подвал на всю ширину окна.

Подсказка: в документе использованы цвета #333, #999 и #fff; остальные значения выберите сами. Меню и основному содержимому задайте внешний отступ слева, равный ширине панели.

Ожидаемый результат: справа в белой шапке пункты «Раздел 3», «Раздел 2», «Раздел 1», «Домой» — в обратном порядке, потому что каждый следующий пункт с float: right встаёт левее предыдущего. Слева тёмная панель высотой 400 пикселей, справа от неё серое меню и белая область с заголовком, внизу серый подвал на всю ширину.

Выполните дополнительную задачу

index.html
HTML
<!doctype html>
<html lang="ru">
  <head>
    <meta charset="utf-8">
    <title>Дополнительное задание</title>
    <style>
      body { margin: 0; }
    </style>
  </head>
  <body>
    <header>
      <ul>
        <li><a href="#">Домой</a></li>
        <li><a href="#">Раздел 1</a></li>
        <li><a href="#">Раздел 2</a></li>
        <li><a href="#">Раздел 3</a></li>
      </ul>
    </header>
    <aside></aside>
    <nav>
      <ul>
        <li><a href="#">Домой</a></li>
        <li><a href="#">Раздел 1</a></li>
        <li><a href="#">Раздел 2</a></li>
        <li><a href="#">Раздел 3</a></li>
      </ul>
    </nav>
    <article>
      <h1>Вёрстка сайта плавающими элементами</h1>
      <p>Основное содержимое страницы.</p>
    </article>
    <footer>2017, basicweb.ru, дополнительное задание.</footer>
  </body>
</html>

История: вёрстка страниц на float

Как раньше верстали главную страницу плавающими элементами

До появления Flexbox и Grid колонки, меню и ряды фото строили на float. В старом коде вы встретите такую разметку главной страницы:

Старая вёрстка: попробуйте сами

Меню шапки, боковое меню и ряд фото построены на float. Сузьте окно: боковое меню шириной 15% становится тесным, а разделы с отступом 16% не перестраиваются — так выглядели неадаптивные страницы.

index.html
HTML
<!doctype html>
<html lang="ru">
  <head>
    <meta charset="utf-8">
    <title>Вёрстка страницы плавающими элементами</title>
    <style>
      body { font-family: system-ui, sans-serif; background-color: linen; color: #2b211b; }
      header {
        background-color: #777;
      }
      header ul {
        list-style-type: none; /* убираем маркеры */
        margin: 0;
        padding: 0;
        overflow: auto; /* шапка включает плавающие пункты */
      }
      header li {
        float: left; /* пункты меню в строку */
        color: white;
      }
      header li a {
        display: inline-block; /* строчной ссылке не задать вертикальные отступы */
        color: orange;
        padding: 10px;
        text-decoration: none;
      }
      header li a:hover {
        background-color: #888;
      }
      footer {
        background-color: #888;
        clear: both; /* подвал не поднимается к плавающим элементам */
        min-height: 30px;
      }
      nav {
        margin-top: 2%;
        float: left; /* боковое меню слева */
        width: 15%;
        border: 1px solid #888;
      }
      nav ul {
        list-style-type: none;
        padding: 0;
      }
      nav li a {
        color: orange;
        padding: 10px;
        text-decoration: none;
      }
      nav li a:hover {
        color: #777;
      }
      section {
        margin: 2% 0 0 16%; /* отступ слева больше ширины меню — ровная колонка */
        border: 1px solid #888;
      }
      img {
        width: 13%;
        height: auto;
        margin: 2% 10%;
        float: left; /* фотографии в ряд */
        border: 1px solid orange;
      }
      .clear-photos {
        clear: left; /* ряд фото начинается под боковым меню */
      }
    </style>
  </head>
  <body>
    <!-- шапка сайта -->
    <header>
      <ul>
        <li><a href="#">Домой</a>|</li>
        <li><a href="#">Раздел 1</a>|</li>
        <li><a href="#">Раздел 2</a>|</li>
        <li><a href="#">Раздел 3</a></li>
      </ul>
    </header>
    <!-- дополнительная навигация -->
    <nav>
      <ul>
        <li><a href="#">Домой</a></li>
        <li><a href="#">Раздел 1</a></li>
        <li><a href="#">Раздел 2</a></li>
        <li><a href="#">Раздел 3</a></li>
      </ul>
    </nav>
    <!-- основное содержимое -->
    <section>Канат с мячиком «Удав» — моя любимая игрушка.</section>
    <section>Резиновая косточка — для вечеров, когда гулять уже поздно.</section>
    <div class="clear-photos">
      <img src="assets/album-puppy.png" alt="Тони-щенок" width="300" height="400">
      <img src="assets/album-sleep.png" alt="Тони спит" width="300" height="319">
      <img src="assets/album-laugh.png" alt="Тони смеётся" width="300" height="400">
    </div>
    <footer>2016, все права принадлежат Тони и сайту basicweb.ru.</footer>
  </body>
</html>

По порядку, что сделано в этом примере:

  • Шапка (<header>) — фон #777. У списка меню убраны маркеры (list-style-type: none) и отступы, а overflow: auto заставляет шапку включить плавающие пункты, — та же особенность, что мы разбирали выше. Пункты (<li>) плавающие и прижаты к левому краю. Ссылки — строчно-блочные (display: inline-block), чтобы задать им внутренние отступы 10px; цвет оранжевый, без подчёркивания, при наведении фон меняется на #888 (псевдокласс :hover).
  • Подвал (<footer>) — фон, минимальная высота 30px и clear: both: плавающие элементы запрещены с обеих сторон, поэтому подвал не поднимается к ним (здесь достаточно и значения left).
  • Боковое меню (<nav>) — внешний отступ сверху 2%, плавающее, прижато к левому краю, ширина 15% от <body> и рамка 1px solid #888. У списка убраны маркеры и внутренние отступы; ссылки оранжевые, без подчёркивания, при наведении серые.
  • Разделы с содержимым (<section>) — внешний отступ слева 16% при ширине меню 15%. Разделы обтекают меню, а отступ выравнивает их ровной колонкой; сверху отступ 2% (margin: 2% 0 0 16%).
  • Фото лежат в блоке с классом .clear-photos и clear: left, поэтому ряд начинается под боковым меню, а не обтекает его. Сами изображения — шириной 13%, с отступами 2% 10%, плавающие, прижаты влево и обведены оранжевой рамкой.

Такая вёрстка требовала подбирать проценты, следить за отменой обтекания и плохо приспосабливалась к узким экранам. Сегодня ту же раскладку собирают сеткой: каркас из шапки, бокового меню, содержимого и подвала — на Grid, меню в строку и ряд фото — на Flexbox.