Пару месяцев назад автора этого материала спросили о проблеме, которая возникла именно из-за этой строки CSS. В решении свойства position не оказалось вообще.

Пока у нас стартует новый поток курса по фронтенду, рассказываем о случаях, когда position: absolute вполне заменим современным CSS.


Введение

Вернёмся на 5 лет назад. Тогда CSS flexbox был новинкой и не мог работать на 100 %, а CSS grid не поддерживался. Мы работали с позиционированием CSS. Но сегодня проблемы тех лет решаются при помощи CSS flexbox или grid.

Наложение карточек

Когда у нас есть карточка, содержащая текст поверх изображения, мы часто используем position: absolute для размещения содержимого поверх изображения. Это больше не нужно при использовании CSS grid.

Вот типичная карточка с текстом над изображением:

<article class="card">
    <div class="card__thumb">
        <img src="assets/mini-cheesecake.jpg" alt="">
    </div>
    <div class="card__content">
        <h2><a href="#">Title</a></h2>
        <p>Subtitle</p>
    </div>
</article>

Чтобы разместить содержимое над изображением, нужно позиционировать .card__content  абсолютно:

.card {
    position: relative;
}

.card__content {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    background: linear-gradient(to top, #000, rgba(0, 0, 0, 0) bottom/100% 60% no-repeat;
    padding: 1rem;
}

Нет ничего плохого в position: absolute из примера выше, но почему не написать проще? Первый шаг — добавить display: gridк компоненту card. Задавать столбцы или строки не нужно:

.card {
    position: relative;
    display: grid;
}

.card__content {
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
}

По умолчанию CSS grid создаёт строки автоматически, на основе контента. В нашей карточке основных элементов два, и поэтому получится две строки контента. Чтобы перекрыть содержимое с изображением, нужно разместить оба элемента на первой области сетки:

.card__thumb,
.card__content {
    grid-column: 1/2;
    grid-row: 1/2;
}

А лучше воспользоваться сокращением grid-area:

.card__thumb,
.card__content {
    grid-area: 1/2;
}

Наконец, мы также можем использовать grid-area: 1/-1.  -1 — последний столбец и строку в сетке, поэтому она всегда будет простираться до конца столбца или строки.

Метка карточки (must try)

Хочется расширить предыдущий пример и включить тег в верхнюю левую область карточки. Для этого нам нужно использовать ту же технику — grid-area: 1/-1, но не хочется, чтобы тег заполнил всю карточку.

Чтобы исправить проблему заполнения, нам нужно указать метке выравнивание по началу контейнера:

.card__tag {
    align-self: start;
    justify-self: start;
    /* Other styles */
}

Познакомьтесь с меткой, которая позиционируется над своим родителем без position: absolute:

Раздел Hero

Ещё один идеальный вариант — раздел hero с контентом, который перекрывает изображение.

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

  • изображение;

  • градиентное наложение;

  • контент.

Это можно сделать разными способами. Если изображение исключительно декоративное, то подходит background-image. В противном случае можно воспользоваться <img>:

.hero {
    position: relative;
    min-height: 500px;
}

.hero__thumb {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* The overlay */
.hero:after {
    content: "";
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    background-color: #000;
    opacity: 0.5;
}

.hero__content {
    position: absolute;
    left: 50%;
    top: 50%;
    z-index: 1;
    transform: translate(-50%, -50%);
    text-align: center;
}

Вот так hero реализуется при помощи абсолютного позиционирования. Но можно сделать лучше.

Прежде всего добавим к hero свойство display: grid. И применим тот же принцип, что в компоненте карточки, то есть пропишем grid-area: 1/-1 в каждом прямом потомке элемента hero.

К сожалению, чтобы .hero__thumb работал, высоту hero придётся фиксировать. Если потомок элемента имеет свойство height: 100%, то его родитель должен иметь явное фиксированное свойство height, а не min-height.

.hero {
    display: grid;
    height: 500px;
}

.hero__content {
    z-index: 1; /* [1] */
    grid-area: 1/-1;
    display: flex;
    flex-direction: column;
    margin: auto; /* [2] */
    text-align: center;
}

.hero__thumb {
    grid-area: 1/-1;
    object-fit: cover; /* [3] */
    width: 100%;
    height: 100%;
    min-height: 0; /* [4] */
}

.hero:after {
    content: "";
    background-color: #000;
    opacity: 0.5;
    grid-area: 1/-1;
}

Пройду по отмеченным строкам, они важны:

  1. Мы можем использовать z-index для элементов grid или flex. При этом нет необходимости добавлять position: relative.

  2. Поскольку .hero__content  — элемент grid, свойство margin: auto центрирует его по горизонтали и вертикали.

  3. Не забудьте включить object-fit: coverпри работе с изображениями.

  4. Я использовал min-height: 0 для изображения на случай, если оно окажется большим. Это нужно, чтобы заставить grid соблюдать height: 100% и не делать изображение больше, чем секция hero, когда используется огромное изображение. Подробнее об этом читайте в статье о минимальном размере контента в CSS grid.

Заметили, что решение стало намного чище?

CSS display: contents

Я думаю, что это первый реальный случай использования display: contents, который был полезен. В примере ниже у нас есть содержимое и изображение.

Вот HTML-код примера:

<div class="hero">
    <div class="hero__content">
        <h2><!-- Title --></h2>
        <p><!-- Desc --></p>
        <a href="#">Order now</a>
    </div>
    <img src="recipe.jpg" alt="">
</div>

До сих пор ничего странного. На мобильных устройствах хочется добиться такого расположения:

Обратите внимание: изображение теперь расположено между заголовком и описанием. Как решить эту проблему? Можно подумать, что разметку нужно изменить так:

<div class="hero">
    <div class="hero__content">
        <h2><!-- Title --></h2>
        <img src="recipe.jpg" alt="">
        <p><!-- Desc --></p>
        <a href="#">Order now</a>
    </div>
</div>

Этот код может работать согласно ожиданиям, а на десктопе нужно расположить <img> справа. Он работает, но есть решение с display: contents и оно проще. Чтобы показать его, вернёмся к исходной разметке:

<div class="hero">
    <div class="hero__content">
        <h2><!-- Title --></h2>
        <p><!-- Desc --></p>
        <a href="#">Order now</a>
    </div>
    <img src="recipe.jpg" alt="">
</div>

Обратите внимание: содержимое обёрнуто внутри .hero__content. Как сообщить браузеру о нашем желании, чтобы <h2>, <p> и <a> стали прямыми потомками <img>? При помощи display: contents:

.hero__content {
    display: contents;
}

Элемент .hero__content теперь — скрытый призрак. Браузер будет парсить разметку вот так:

<div class="hero">
    <h2><!-- Title --></h2>
    <p><!-- Desc --></p>
    <a href="#">Order now</a>
    <img src="recipe.jpg" alt="">
</div>

И вот всё, что нам нужно сделать в CSS:

.hero {
    display: flex;
    flex-direction: column;
}

.hero__content {
    display: contents;
}

.hero h2,
.hero img {
    order: -1;
}

При правильном применении display: contents — это мощная техника, позволяющая добиться того, что ещё несколько лет назад было невозможно. Конечно, мы хотим так же обращаться со стилями десктопа:

@media (min-width: 750px) {
    .hero {
        flex-direction: row;
    }
    
    .hero__content {
        display: initial;
    }
    
    .hero h2,
    .hero img {
        order: initial;
    }
}

Упорядочиваем элементы компонента карточки

Ниже код варианта карточки, где заголовок размещается в её верхней части. Посмотрим на HTML:

<article class="card">
    <img src="thumb.jpg" alt="">
    <div class="card__content">
        <h3>Title</h3>
        <p>Description</p>
        <p>Actions</p>
    </div>
</article>

Обратите внимание: у нас есть два прямых потомка card: изображение thumb.jpg и div с классом card__content. Как с такой разметкой расположить заголовок <h3> вверху? Это можно сделать с помощью абсолютного позиционирования:

.card {
    position: relative;
    padding-top: 3rem;
    /* Accommodate for the title space */
}

.card h3 {
    position: absolute;
    left: 1rem;
    top: 1rem;
}

Однако это решение может не работать, когда заголовок слишком длинный.

Это происходит потому, что заголовок находится вне обычного потока, а значит, браузеру не важна его длина. При помощи display: contents можно добиться большего:

.card {
    display: flex;
    flex-direction: column;
    padding: 1rem;
}

.card__content {
    display: contents;
}

.card h3 {
    order: -1;
}

Это позволяет контролировать все дочерние элементы, при необходимости гибко настраивая их порядок свойством order.

Есть небольшая проблема: мы добавили padding: 1rem к родительскому элементу, поэтому изображение должно прилипать к краям. Вот как исправить ситуацию:

.card img {
    width: calc(100% + 2rem);
    margin-left: -1rem;
}

Центрирование

Можно встретить много шуток о том, что центрирование — это сложно. Но в наши дни центрировать элемент проще, чем когда-либо. Больше не нужно использовать position: absolute с transform. К примеру, для центрирования flex-элемента по горизонтали и по вертикали можно воспользоваться margin: auto:

.card {
    display: flex;
}

.card__image {
    margin: auto;
}

Я написал полное руководство по центрированию в CSS.

Соотношение сторон изображения

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

.card__thumb {
  position: relative;
  padding-top: 75%;
}

.card__thumb img {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

С aspect-ratio соблюдать соотношения проще:

/*  */
.card__thumb {
  position: relative;
  aspect-ratio: 4/3;
}

Подробности смотрите здесь.

Иногда лучше position: absolute

В этом примере содержимое (аватар, имя и ссылка) перекрывается обложкой карточки. У нас есть два варианта:

  1. position: absolute для обложки карточки;

  2. отрицательный margin для содержимого.

Посмотрим на два других решения. Какое из них больше подходит для нашего случая?

Использование абсолютного позиционирования

Так мы можем позиционировать прямоугольник абсолютно, а затем добавить padding: 1rem к содержимому карточки:

.card__cover {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 50px;
}

.card__content {
    padding: 1rem;
}

Таким образом, когда обложка карточки будет убрана, нам не придётся изменять CSS.

Использование отрицательного margin

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

.card__content {
    padding: 1rem;
    margin-top: -1rem;
}

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

Обратите внимание, что аватар находится за пределами своего родителя (карточки). Чтобы исправить это, изменим CSS и уберём отрицательный margin:

.card--no-cover .card__content {
    margin-top: 0;
}

Лучшим решением оказалось position: absolute. Здесь это свойство избавляет от лишнего CSS.

Дополнительные материалы

  • Райан Маллиган опубликовал отличную статью о позиционировании оверлейного контента с помощью CSS-grid.

  • Стефани Эклз написала замечательную статью об использовании CSS-grid для построения разделов hero.

Разобраться в современном CSS вы сможете на наших курсах:

Другие профессии и курсы

Комментарии (12)


  1. XeL077
    18.11.2021 01:05
    +12

    position: absolute; выглядит гораздо проще "магических чисел" в css. Конечно можно сделать миксин и все такое, но если нет проблем с композитными слоями не вижу причины переходить на гриды.


    1. antsam
      18.11.2021 23:22

      Может это дело привычки, но помоему

      .hero__content {
          grid-area: 1/2;
          justify-self: center;
          align-self: center;
      }

      выглядит гараздо понятнее, чем заклинание:

      .hero__content {
          position: absolute;
          left: 50%;
          top: 50%;
          transform: translate(-50%, -50%);
      }

      При этом в первом случае у меня остался неисползуемый "transform", который я могу использовать например для анимации.


      1. freekir
        22.11.2021 01:31

        Вы абсолютно правы, дело привычки. Кому-то и хаки для ие были норм. А кому-то эти дроби в гридах мутной темой кажутся.

        Как по мне главное чтобы улучшение было оправдано, а не технологии, ради технологий.

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


  1. mSnus
    18.11.2021 06:53
    +10

    Вот увидел бы такой css, мысль была бы одна: "здесь творится какая-то чертова магия". Ради чего городить grid и включать довольно неочевидный display: content? Ради экономии пара строчек и погони за модой?

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

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


  1. extempl
    18.11.2021 08:26
    +1

    Ну скажем, с гридами в области решения hero секции подход интересный. Но вот `display: contents` - это и правда неочевидная фича и костыль. Зато стильно-модно-молодёжно.

    Достаточно убрать `__content` вообще и использовать `order` as-is.


  1. Exclipt
    18.11.2021 11:19

    Хорошо иметь подобный уровень знаний, но его надо применять во благо. У вас в статье классический пример "усложнения кода ради великого ничего". У меня в команде так самые молодые любят писать, чтобы показать, какие они грамотные, не задумываясь о том, что другие молодые, которым это саппортить, могут быть не такие уж и грамотные, или вообще просто "Вася, оторвись на секунду от бэкэнда, надо картинку передвинуть".


  1. amvasiljev
    18.11.2021 13:13

    не стоит сжигать дом, ради того, чтобы поджарить пару яиц ))

    но решение интересное


  1. korsetlr473
    18.11.2021 16:06
    +1

    Ну и треш....

    Наложение , а не позиционирование алё

    "почему не написать проще? Первый шаг — добавить display: gridк компоненту card." дальше даже не читал....

    ужас .


  1. antsam
    18.11.2021 23:55

    Лично я свалил с абсолютного позиционирования из-за бесконечных проблем с z-index.

    Как только начинается position: relative, то он внезапно становится поверх обычных элементов и создает свой контекст наложения, потом появляются z-index-ы, потом приходится вручную продумывать и конфигурировать какой элемент над каким находится. Вот тогда и начинается ужас.

    Намного проще отдать эти расчеты браузеру просто расположив элементы в правильном месте в DOM дереве.

    До флексов и гридов от абсолютного позиционирования нельзя было уйти, но после их появелния у меня лично сомненией не возникло - не хочется повторять негативный опыт с z-index.

    Ну и не знаю как сейчас, но года 4 назад были жуткие тормоза из-за "position: relative" на мобильніх устройствах и переписывали тормозные участки на флексы.


    1. wadowad
      19.11.2021 11:22

      Если у слоя, который должен быть позиционирован внизу, назначить z-index: -1, то не нужно будет у всего остального прописывать position: relative. Да, есть минусы такого решения. Например, у родителя не должно быть фона.


    1. freekir
      22.11.2021 01:34

      Наверное у вас какие-то очень сложные проекты.


  1. antsam
    19.11.2021 00:04

    мне кстати запись

    grid-area: 1/2;

    неочень нравится и действительно не очень понятна

    Я у себя делаю именованный grid-area:

    .layers{
      display: grid;
      grid-template: "layers" auto / auto;
      > *, > ::after, > ::before {
            grid-area: layers;
      }
    }

    Сразу понятно что в этом элементе будут слои расположенные друг над другом.