🔍 Начните печатать, чтобы искать по книге или перейти к нужной странице по номеру

Удобно листать не только прокруткой, но и клавишами‑стрелками:

 
между важными местами
Shift
между
разворотами
Васи­лий Полов­нёв, Игорь Пет­ров

ХТМЛ.
Вёрстка сайтов

Изда­тель­ство Бюро Гор­бу­нова
2021
Василий Половнёв, Игорь Петров

ХТМЛ.
Вёрстка сайтов

Издательство Бюро Горбунова
2021
удк 004.42
ббк З973.42
П52
Васи­лий Полов­нёв, Игорь Пет­ров
П52
ХТМЛ. Вёрстка сай­тов для дизай­не­ров, редак­то­ров и руко­во­ди­те­лей.—
М.: Изд‑во Бюро Гор­бу­нова, 2021

Эта книга — поша­го­вая инструк­ция по вёрстке сай­тов на язы­ках ХТМЛ и ЦСС. Вы узна­ете не только как свер­стать сайт, но и как опуб­ли­ко­вать его в интер­нете, настро­ить кра­си­вый шаринг в соц­сети и под­клю­чить системы аналитики.

Отдель­ный раз­дел книги посвя­щён работе с вер­сталь­щи­ком: как ста­вить задачи, полу­чать пред­ска­зу­е­мый резуль­тат и пра­вильно при­ни­мать и внед­рять вёрстку.

Оглавление

удк 004.42
ббк З973.42
П52
П52
Василий Половнёв, Игорь Петров
ХТМЛ. Вёрстка сайтов для дизайнеров, редакторов и руководителей.—
М.: Изд‑во Бюро Горбунова, 2021

Эта книга — пошаговая инструкция по вёрстке сайтов на языках ХТМЛ и ЦСС. Вы узнаете не только как сверстать сайт, но и как опубликовать его в интернете, настроить красивый шаринг в соцсети и подключить системы аналитики.

Отдельный раздел книги посвящён работе с верстальщиком: как ставить задачи, получать предсказуемый результат и правильно принимать и внедрять вёрстку.

Оглавление

Скрыто 169 разворотов

Точки излома

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

Точка излома, breakpoint, — ширина экрана, при кото­рой одна вер­сия вёрстки сме­ня­ется на дру­гую. Напри­мер, при ширине экрана менее 991 пик­селя меню могло бы занять всю ширину экрана и стать про­кру­чи­ва­е­мым. Исполь­зуем для этого медиавыражения.

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

В боль­шин­стве попу­ляр­ных ЦСС‑фрейм­вор­ков эти точки выгля­дят так:

Узкие мобильные< 576 пк
Мобильные< 768 пк
Лаптопы≥ 768 пк
Десктопы≥ 992 пк
Широ­кие десктопы≥ 1400 пк

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

Напри­мер, на сайте бюро мы оста­но­ви­лись на трёх типах устройств:

Мобильные≤ 960 пк
Лаптопы> 960 пк
Десктопы> 1400 пк

Сове­туем исполь­зо­вать как можно меньше устройств и точек излома. Чем их больше, тем больше вари­ан­тов рас­кладки нужно при­ду­мать, свер­стать и про­ве­рить. Если шапка и под­вал адап­ти­ру­ются к двум устрой­ствам, это даёт 2×2 = 4 вари­анта рас­кладки. Если шапка и под­вал адап­ти­ру­ются к пяти устрой­ствам: это даёт 2×5 = 10 вари­ан­тов раскладки.

Макбук, 1440×900

Айпад, 820×1180

Айфон, 393×852

Макбук, 1440×900

Айпад, 820×1180

Айфон, 393×852

Точки излома

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

Точка излома, breakpoint, — ширина экрана, при которой одна версия вёрстки сменяется на другую. Например, при ширине экрана менее 991 пикселя меню могло бы занять всю ширину экрана и стать прокручиваемым. Используем для этого медиавыражения.

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

В большинстве популярных ЦСС‑фреймворков эти точки выглядят так:

Узкие мобильные< 576 пк
Мобильные< 768 пк
Лаптопы≥ 768 пк
Десктопы≥ 992 пк
Широкие десктопы≥ 1400 пк

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

Например, на сайте бюро мы остановились на трёх типах устройств:

Мобильные≤ 960 пк
Лаптопы> 960 пк
Десктопы> 1400 пк

Советуем использовать как можно меньше устройств и точек излома. Чем их больше, тем больше вариантов раскладки нужно придумать, сверстать и проверить. Если шапка и подвал адаптируются к двум устройствам, это даёт 2×2 = 4 варианта раскладки. Если шапка и подвал адаптируются к пяти устройствам: это даёт 2×5 = 10 вариантов раскладки.

Большая. Новодмитровская улица., 36, строение. 2
Москва, Россия, 127015

Адап­та­ция может пона­до­биться не только на уровне всего сайта, но и на уровне отдель­ных эле­мен­тов. Напри­мер, адрес ком­па­нии в под­вале может посте­пенно сокра­щать свой текст, когда пере­стаёт помещаться:

Боль­шая Новод­мит­ров­ская улица, 36, стро­е­ние 2
Боль­шая Новод­мит­ров­ская улица, 36, стр. 2
Б. Новод­мит­ров­ская ул., 36, стр. 2

В таких слу­чаях точки излома лучше выби­рать исходя из содер­жи­мого и исполь­зо­вать Container Queries — медиа­вы­ра­же­ния, оттал­ки­ва­ю­щи­еся от раз­ме­ров кон­тей­нера, а не от раз­ме­ров окна:

<footer>
  <div class="address">
    Б<span class="sacrifice--second">ольшая</span><span>.</span> Новодмитровская ул<span class="sacrifice--second">ица</span><span>.</span>,
    36, стр<span class="sacrifice--first">оение</span><span>.</span> 2
  </div>
</footer>

Для логичных переносов склеили неразрывными пробелами части адреса, сильно связанные друг с другом по смыслу: «Большая Новодмитровская улица», «36, строение 2»

.address {
  container-type: inline-size; /* Объявляем .address контейнером, с шириной которого можно делать медиавыражения */
}

.address .sacrifice--first + span,
.address .sacrifice--first + span {
  display: none; /* Скрываем спаны с точками, пока они не нужны */
}

@container (width <= 21em) { /* При ширине подвала менее 21em, используем первую волну сокращений */
  .address .sacrifice--first {
    display: none;
  }

  .address .sacrifice--first + span {
    display: inline;
  }
}

@container (width <= 19em) { /* При ширине подвала менее 19em, используем вторую волну сокращений */
  .address .sacrifice--second {
    display: none;
  }

  .address .sacrifice--second + span {
    display: inline;
  }
}

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

Большая. Новодмитровская улица., 36, строение. 2
Москва, Россия, 127015

Адаптация может понадобиться не только на уровне всего сайта, но и на уровне отдельных элементов. Например, адрес компании в подвале может постепенно сокращать свой текст, когда перестаёт помещаться:

Большая Новодмитровская улица, 36, строение 2
Большая Новодмитровская улица, 36, стр. 2
Б. Новодмитровская ул., 36, стр. 2

В таких случаях точки излома лучше выбирать исходя из содержимого и использовать Container Queries — медиавыражения, отталкивающиеся от размеров контейнера, а не от размеров окна:

<footer>
  <div class="address">
    Б<span class="sacrifice--second">ольшая</span><span>.</span> Новодмитровская ул<span class="sacrifice--second">ица</span><span>.</span>,
    36, стр<span class="sacrifice--first">оение</span><span>.</span> 2
  </div>
</footer>

Для логичных переносов склеили неразрывными пробелами части адреса, сильно связанные друг с другом по смыслу: «Большая Новодмитровская улица», «36, строение 2»

.address {
  container-type: inline-size; /* Объявляем .address контейнером, с шириной которого можно делать медиавыражения */
}

.address .sacrifice--first + span,
.address .sacrifice--first + span {
  display: none; /* Скрываем спаны с точками, пока они не нужны */
}

@container (width <= 21em) { /* При ширине подвала менее 21em, используем первую волну сокращений */
  .address .sacrifice--first {
    display: none;
  }

  .address .sacrifice--first + span {
    display: inline;
  }
}

@container (width <= 19em) { /* При ширине подвала менее 19em, используем вторую волну сокращений */
  .address .sacrifice--second {
    display: none;
  }

  .address .sacrifice--second + span {
    display: inline;
  }
}

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

Шапки, меню, подвалы

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

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

Шапки, меню, подвалы

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

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

Типичная шапка из трёх элементов: логотипа, меню и телефона

В контейнере <nav> — navigation — собирают ссылки для навигации по сайту

Если не свешивать круглый логотип за линию сетки, он будет казаться сдвинутым

Шапка и подвал сайта чаще всего имеют форму строк, прижатых к верху и низу страницы

Мы используем max‑content, а не min‑content, чтобы пункты меню из нескольких слов не переносились

Аналогично можно было бы выделить текущий пункт меню начертанием или цветом

По умолчанию содержимое грид‑ячеек выравнивается по верхнему краю

В гридах align‑items управляет выравниванием по вертикали, а justify‑items — по горизонтали

В гридах justify‑self управляет выравниванием внутри ячейки

Иногда подчёркивание делают с помощью border‑bottom, background‑image или box‑shadow

Свер­стаем про­стую шапку «в линию», встре­ча­ю­щу­юся на боль­шин­стве сай­тов. Сосре­до­то­чимся только на рас­кладке, опу­стив типо­гра­фику и стили для сброса.

Нач­нём с раз­метки. Собе­рём шапку в кон­тей­нере <header>. Для лого­типа возь­мём див с кар­тин­кой, завёр­ну­той в ссылку на глав­ную стра­ницу. Меню собе­рём в нену­ме­ро­ван­ный спи­сок <ul>. Теле­фон сде­лаем кли­ка­бель­ной ссыл­кой с хит­рым про­то­ко­лом tel:

<header class="header">
  <div class="logo">
    <a href="/">
      <img src="logo.svg">
    </a>
  </div>

  <nav class="nav">
    <ul>
      <li class="nav__item">
        <a href="/products/">Продукция</a>
      </li>
      <li class="nav__item">
        <a href="/services/">Услуги</a>
      </li>
      <li class="nav__item is__active">
        <a href="/blog/">Блог</a>
      </li>
      <li class="nav__item">
        <a href="/contacts/">Контакты</a>
      </li>
    </ul>
  </nav>

  <div class="tel">
    <a href="tel:+74954005050">
      +7 495 400‑50‑50
    </a>
  </div>
</header>

Стилизуем логотип. Зафик­си­руем его раз­меры и све­сим кру­жок лого­типа за линии сетки:

.logo {
  width: 40px;
  height: 40px;
  margin: 0 -5px;
}

Стилизуем меню. Вклю­чим гриды и раз­ло­жим пункты меню по колонкам:

.nav ul {
  display: grid;
  grid-auto-flow: column;
}

Опре­де­лим авто­ма­ти­че­ские колонки по мак­си­маль­ному содер­жи­мому и зада­дим отступ между пунк­тами меню:

.nav ul {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: max-content;
  column-gap: 20px;
}

Стилизуем пункты меню. Уве­ли­чим кли­ка­бель­ную область ссы­лок и сти­ли­зуем теку­щий пункт меню:

.nav ul {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: max-content;
  column-gap: 20px;
  align-items: center;
}

.nav__item a {
  position: relative;
  z-index: 1;
}

.nav__item a::before {
  content: '';
  position: absolute;
  inset: -3px -7px;
  z-index: -1;
}

.nav__item.is__active a {
  color: #fff;
  text-decoration: none;
}

.nav__item.is__active a::before {
  background-color: #ff5722;
  border-radius: 8px;
}

Настроим раскладку шапки в целом. Вклю­чим гриды и раз­ло­жим лого­тип, меню и теле­фон по колон­кам. Заодно доба­вим шапке верх­ний падинг и сде­лаем её кон­тей­не­ром‑обёрт­кой, чтобы она не при­ли­пала к краям и пра­вильно управ­ляла своей шириной:

.header {
  display: grid;
  grid-template-columns: min-content max-content 1fr;
  column-gap: 20px;
  padding-top: 10px;
}
<header class="header layoutContainer">
  <!-- … -->
</header>

Отцен­три­руем всё по вертикали:

.header {
  display: grid;
  grid-template-columns: min-content max-content 1fr;
  column-gap: 20px;
  padding-top: 10px;
  align-items: center;
  line-height: 1;
}

Ото­жмём теле­фон к пра­вому краю:

.header {
  display: grid;
  grid-template-columns: min-content max-content 1fr;
  column-gap: 20px;
  padding: 10px;
  align-items: center;
}

.header .tel {
  justify-self: end;
}

Убе­рём под­чер­ки­ва­ния у ссы­лок в шапке:

.header {
  display: grid;
  grid-template-columns: min-content max-content 1fr;
  column-gap: 20px;
  padding: 10px;
  align-items: center;
}

.header .tel {
  justify-self: end;
}

.header a {
  text-decoration: none;
}

Типичная шапка из трёх элементов: логотипа, меню и телефона

В контейнере <nav> — navigation — собирают ссылки для навигации по сайту

Если не свешивать круглый логотип за линию сетки, он будет казаться сдвинутым

Шапка и подвал сайта чаще всего имеют форму строк, прижатых к верху и низу страницы

Мы используем max‑content, а не min‑content, чтобы пункты меню из нескольких слов не переносились

Аналогично можно было бы выделить текущий пункт меню начертанием или цветом

По умолчанию содержимое грид‑ячеек выравнивается по верхнему краю

В гридах align‑items управляет выравниванием по вертикали, а justify‑items — по горизонтали

В гридах justify‑self управляет выравниванием внутри ячейки

Иногда подчёркивание делают с помощью border‑bottom, background‑image или box‑shadow

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

Начнём с разметки. Соберём шапку в контейнере <header>. Для логотипа возьмём див с картинкой, завёрнутой в ссылку на главную страницу. Меню соберём в ненумерованный список <ul>. Телефон сделаем кликабельной ссылкой с хитрым протоколом tel:

<header class="header">
  <div class="logo">
    <a href="/">
      <img src="logo.svg">
    </a>
  </div>

  <nav class="nav">
    <ul>
      <li class="nav__item">
        <a href="/products/">Продукция</a>
      </li>
      <li class="nav__item">
        <a href="/services/">Услуги</a>
      </li>
      <li class="nav__item is__active">
        <a href="/blog/">Блог</a>
      </li>
      <li class="nav__item">
        <a href="/contacts/">Контакты</a>
      </li>
    </ul>
  </nav>

  <div class="tel">
    <a href="tel:+74954005050">
      +7 495 400‑50‑50
    </a>
  </div>
</header>

Стилизуем логотип. Зафиксируем его размеры и свесим кружок логотипа за линии сетки:

.logo {
  width: 40px;
  height: 40px;
  margin: 0 -5px;
}

Стилизуем меню. Включим гриды и разложим пункты меню по колонкам:

.nav ul {
  display: grid;
  grid-auto-flow: column;
}

Определим автоматические колонки по максимальному содержимому и зададим отступ между пунктами меню:

.nav ul {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: max-content;
  column-gap: 20px;
}

Стилизуем пункты меню. Увеличим кликабельную область ссылок и стилизуем текущий пункт меню:

.nav ul {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: max-content;
  column-gap: 20px;
  align-items: center;
}

.nav__item a {
  position: relative;
  z-index: 1;
}

.nav__item a::before {
  content: '';
  position: absolute;
  inset: -3px -7px;
  z-index: -1;
}

.nav__item.is__active a {
  color: #fff;
  text-decoration: none;
}

.nav__item.is__active a::before {
  background-color: #ff5722;
  border-radius: 8px;
}

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

.header {
  display: grid;
  grid-template-columns: min-content max-content 1fr;
  column-gap: 20px;
  padding-top: 10px;
}
<header class="header layoutContainer">
  <!-- … -->
</header>

Отцентрируем всё по вертикали:

.header {
  display: grid;
  grid-template-columns: min-content max-content 1fr;
  column-gap: 20px;
  padding-top: 10px;
  align-items: center;
  line-height: 1;
}

Отожмём телефон к правому краю:

.header {
  display: grid;
  grid-template-columns: min-content max-content 1fr;
  column-gap: 20px;
  padding: 10px;
  align-items: center;
}

.header .tel {
  justify-self: end;
}

Уберём подчеркивания у ссылок в шапке:

.header {
  display: grid;
  grid-template-columns: min-content max-content 1fr;
  column-gap: 20px;
  padding: 10px;
  align-items: center;
}

.header .tel {
  justify-self: end;
}

.header a {
  text-decoration: none;
}

Адап­ти­руем полу­чив­шу­юся шапку для мобиль­ных устройств. Будем счи­тать, что в нашем дизайне мобиль­ные устрой­ства — это устрой­ства с экра­ном шири­ной менее 992 пикселей.

Обер­нём <nav> в див с клас­сом navHolder — он при­го­дится нам для про­крутки и обре­за­ния меню:

<header class="header">
  <div class="logo">…</div>

  <div class="navHolder">
    <nav class="nav">
      <!-- … -->
    </nav>
  </div>

  <div class="tel">…</div>
</header>

Поме­няем рас­кладку шапки. Вме­сто трёх коло­нок сде­лаем сетку 2×2:

@media (width < 992px) {
  .header {
    grid-template-columns: min-content 1fr;
    grid-template-rows: min-content min-content;
    row-gap: 15px;
  }
}

Пере­ста­вим теле­фон в пра­вый верх­ний угол, меню поста­вим вниз и рас­тя­нем на две колонки:

@media (width < 992px) {
  .header {
    grid-template-columns: min-content 1fr;
    grid-template-rows: min-content min-content;
    row-gap: 15px;
  }
  
  .header .tel {
    grid-column: 2;
    grid-row: 1;
  }
  
  .header .navHolder {
    grid-column: 1 / span 2;
    grid-row: 2;
  }
}

Рас­тя­нем меню на всю ширину экрана:

@media (width < 992px) {
  .header {
    grid-template-columns: min-content 1fr;
    grid-template-rows: min-content min-content;
    row-gap: 15px;
  }
  
  .header .tel {
    grid-column: 2;
    grid-row: 1;
  }
  
  .header .navHolder {
    grid-column: 1 / span 2;
    grid-row: 2;
    margin-left: -10px;
    margin-right: -10px;
  }
}

Вклю­чим гори­зон­таль­ную про­крутку в меню:

@media (width < 992px) {
  .header {
    grid-template-columns: min-content 1fr;
    grid-template-rows: min-content min-content;
    row-gap: 15px;
  }
  
  .header .tel {
    grid-column: 2;
    grid-row: 1;
  }
  
  .header .navHolder {
    grid-column: 1 / span 2;
    grid-row: 2;
    margin-left: -10px;
    margin-right: -10px;
  }
  
  .nav {
    overflow-x: scroll; /* Зададим возможность горизонтальной прокрутки */
    -webkit-overflow-scrolling: touch; /* Для Сафари дополнительно зададим инерцию при прокрутке. С инерцией меню будет «оттягиваться» при попытке прокрутить меню за его пределы. */
  }
}

Из‑за overflow обре­за­лись поля у теку­щего пункта в меню. Плюс при про­крутке появ­ля­ется гори­зо­наль­ный скрол­бар. Устра­ним эти проблемы:

@media (width < 992px) {
  .header {
    grid-template-columns: min-content 1fr;
    grid-template-rows: min-content min-content;
    row-gap: 15px;
  }
  
  .header .tel {
    grid-column: 2;
    grid-row: 1;
  }
  
  .header .navHolder {
    grid-column: 1 / span 2;
    grid-row: 2;
    margin-left: -10px;
    margin-right: -10px;
  }
  
  .nav {
    padding: 10px; /* Добавим дополнительные поля */
    margin: -10px 0; /* С помощью отрицательных отступов сверху-снизу компенсируем «нарощенные» поля */
    scrollbar-width: none; /* Скроем скролбар в Хроме и Файрфоксе */
    overflow-x: scroll;
    -webkit-overflow-scrolling: touch;
  }

  .nav::-webkit-scrollbar {
    display: none; /* Скроем скролбар в Сафари */
  }
}

Доба­вим немного кос­ме­тики в виде «забе­ле­ния» по краям с помо­щью маски и градиента:

@media (width < 992px) {
  .header {
    grid-template-columns: min-content 1fr;
    grid-template-rows: min-content min-content;
    row-gap: 15px;
  }
  
  .header .tel {
    grid-column: 2;
    grid-row: 1;
  }
  
  .header .navHolder {
    grid-column: 1 / span 2;
    grid-row: 2;
    margin-left: -10px;
    margin-right: -10px;
  }

  .nav {
    padding: 10px;
    margin: -10px 0;
    scrollbar-width: none;
    overflow-x: scroll;
    -webkit-overflow-scrolling: touch;
    mask-image: linear-gradient(
      90deg,
      transparent 0,
      rgba(0, 0, 0, .25) 8px,
      #000 16px,
      #000 calc(100% - 16px),
      rgba(0, 0, 0, .25) calc(100% - 8px),
      transparent
    );
  }

  .nav::-webkit-scrollbar {
    display: none;
  }
}

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

@media (width < 992px) {
  .navHolder {
    position: relative; /* Мы используем обертку для линеек, потому что линейки у .nav попадали бы под маску и забеление */
  }

  .navHolder::before,
  .navHolder::after {
    content: ''; /* Используем псевдоэлементы, а не рамку, потому что их позицией можно управлять */
    position: absolute;
    left: 0;
    right: 0;
    height: 1px;
    background: rgba(0, 0, 0, .07);
  }

  .navHolder::before {
    top: -9px;
  }

  .navHolder::after {
    bottom: -8px;
  }
}

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

Если меню ста­тич­ное и вряд ли будет меняться, проще задать под­хо­дя­щие зна­че­ния column-gap для раз­ной ширины экрана.

Адаптируем получившуюся шапку для мобильных устройств. Будем считать, что в нашем дизайне мобильные устройства — это устройства с экраном шириной менее 992 пикселей.

Обернём <nav> в див с классом navHolder — он пригодится нам для прокрутки и обрезания меню:

<header class="header">
  <div class="logo">…</div>

  <div class="navHolder">
    <nav class="nav">
      <!-- … -->
    </nav>
  </div>

  <div class="tel">…</div>
</header>

Поменяем раскладку шапки. Вместо трёх колонок сделаем сетку 2×2:

@media (width < 992px) {
  .header {
    grid-template-columns: min-content 1fr;
    grid-template-rows: min-content min-content;
    row-gap: 15px;
  }
}

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

@media (width < 992px) {
  .header {
    grid-template-columns: min-content 1fr;
    grid-template-rows: min-content min-content;
    row-gap: 15px;
  }
  
  .header .tel {
    grid-column: 2;
    grid-row: 1;
  }
  
  .header .navHolder {
    grid-column: 1 / span 2;
    grid-row: 2;
  }
}

Растянем меню на всю ширину экрана:

@media (width < 992px) {
  .header {
    grid-template-columns: min-content 1fr;
    grid-template-rows: min-content min-content;
    row-gap: 15px;
  }
  
  .header .tel {
    grid-column: 2;
    grid-row: 1;
  }
  
  .header .navHolder {
    grid-column: 1 / span 2;
    grid-row: 2;
    margin-left: -10px;
    margin-right: -10px;
  }
}

Включим горизонтальную прокрутку в меню:

@media (width < 992px) {
  .header {
    grid-template-columns: min-content 1fr;
    grid-template-rows: min-content min-content;
    row-gap: 15px;
  }
  
  .header .tel {
    grid-column: 2;
    grid-row: 1;
  }
  
  .header .navHolder {
    grid-column: 1 / span 2;
    grid-row: 2;
    margin-left: -10px;
    margin-right: -10px;
  }
  
  .nav {
    overflow-x: scroll; /* Зададим возможность горизонтальной прокрутки */
    -webkit-overflow-scrolling: touch; /* Для Сафари дополнительно зададим инерцию при прокрутке. С инерцией меню будет «оттягиваться» при попытке прокрутить меню за его пределы. */
  }
}

Из‑за overflow обрезались поля у текущего пункта в меню. Плюс при прокрутке появляется горизональный скролбар. Устраним эти проблемы:

@media (width < 992px) {
  .header {
    grid-template-columns: min-content 1fr;
    grid-template-rows: min-content min-content;
    row-gap: 15px;
  }
  
  .header .tel {
    grid-column: 2;
    grid-row: 1;
  }
  
  .header .navHolder {
    grid-column: 1 / span 2;
    grid-row: 2;
    margin-left: -10px;
    margin-right: -10px;
  }
  
  .nav {
    padding: 10px; /* Добавим дополнительные поля */
    margin: -10px 0; /* С помощью отрицательных отступов сверху-снизу компенсируем «нарощенные» поля */
    scrollbar-width: none; /* Скроем скролбар в Хроме и Файрфоксе */
    overflow-x: scroll;
    -webkit-overflow-scrolling: touch;
  }

  .nav::-webkit-scrollbar {
    display: none; /* Скроем скролбар в Сафари */
  }
}

Добавим немного косметики в виде «забеления» по краям с помощью маски и градиента:

@media (width < 992px) {
  .header {
    grid-template-columns: min-content 1fr;
    grid-template-rows: min-content min-content;
    row-gap: 15px;
  }
  
  .header .tel {
    grid-column: 2;
    grid-row: 1;
  }
  
  .header .navHolder {
    grid-column: 1 / span 2;
    grid-row: 2;
    margin-left: -10px;
    margin-right: -10px;
  }

  .nav {
    padding: 10px;
    margin: -10px 0;
    scrollbar-width: none;
    overflow-x: scroll;
    -webkit-overflow-scrolling: touch;
    mask-image: linear-gradient(
      90deg,
      transparent 0,
      rgba(0, 0, 0, .25) 8px,
      #000 16px,
      #000 calc(100% - 16px),
      rgba(0, 0, 0, .25) calc(100% - 8px),
      transparent
    );
  }

  .nav::-webkit-scrollbar {
    display: none;
  }
}

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

@media (width < 992px) {
  .navHolder {
    position: relative; /* Мы используем обертку для линеек, потому что линейки у .nav попадали бы под маску и забеление */
  }

  .navHolder::before,
  .navHolder::after {
    content: ''; /* Используем псевдоэлементы, а не рамку, потому что их позицией можно управлять */
    position: absolute;
    left: 0;
    right: 0;
    height: 1px;
    background: rgba(0, 0, 0, .07);
  }

  .navHolder::before {
    top: -9px;
  }

  .navHolder::after {
    bottom: -8px;
  }
}

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

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

Скрыто 46 разворотов