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

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

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

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

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

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

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

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

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

Оглавление

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

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

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

Оглавление

Скрыто 170 разворотов
Большая. Новодмитровская улица., 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 для разной ширины экрана.

Адап­ти­руем шапку к мобиль­ной вер­сии с помо­щью гам­бур­гер­ного меню. Будет скры­вать меню и пока­зы­вать «гам­бур­гер» при той ширине экрана, при кото­рой меню не поме­ща­ется в него целиком.

Возь­мём шапку без мобиль­ной вер­сии и доба­вим кнопку гамбургер:

<header class="header">
  <div class="logo">
    <a href="/">
      <img src="logo.svg" width="40" height="40">
    </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>
      <li class="nav__item">
        <a href="tel:+74954005050">+7 495 400‑50‑50</a>
      </li>
    </ul>
  </nav>
  
  <div class="tel">
    <a href="tel:+74954005050">
      +7 495 400‑50‑50
    </a>
  </div>

  <!-- aria-label подскажет смысл кнопки программам чтения с экрана -->
  <button class="hamburger" type="button" aria-label="Открыть меню">
    <span class="hamburger-box">
      <span class="hamburger-inner"></span>
    </span>
  </button>
</header>

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

@media (width > 690px) {
  .header .hamburger {
    display: none;
  }
}

@media (width <= 690px) {
  .header {
    position: relative; /* Задаём контекст для позиционирования меню */
    grid-template-columns: min-content 1fr 1fr min-content; /* Определяем четыре колонки: первую и последнюю — по ширине логотипа и кнопки, вторую и третью — 50% от оставшегося места */
  }

  .header .hamburger {
    grid-column: 4; /* Кнопку ставим в последнюю колонку */
  }

  .header .tel {
    grid-column: 2 / span 2; /* Телефону отдаём две центральные колонки */
    justify-self: stretch;
  }

  .nav {
    position: absolute; /* Меню позиционируем абсолютно, сразу после шапки поверх содержимого. Из-за этого кнопка перестанет нажиматься, исправим это на следующем шаге */
    top: 0;
    left: 0;
    width: 100%; /* Растягиваем меню на всю ширину экрана */
    padding: 65px 10px 20px; /* Добавляем настолько большой верхний падинг, чтобы меню встало точно под шапкой */
  }

  .nav ul {
    grid-auto-flow: row; /* Раскладываем меню рядами */
    grid-auto-rows: max-content;
    row-gap: 10px;
  }
}

Сти­ли­зуем кнопку. Для про­стоты возь­мём гото­вые стили и ани­ма­цию из биб­лио­теки «гам­бур­гер­ных» ико­нок на ЦСС:

<link rel="stylesheet" src="styles/hamburgers.css">

<header class="header">
  <div class="logo">
    <a href="/">
      <img src="logo.svg" width="40" height="40">
    </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>
      <li class="nav__item">
        <a href="tel:+74954005050">+7 495 400‑50‑50</a>
      </li>
    </ul>
  </nav>
  
  <div class="tel">
    <a href="tel:+74954005050">
      +7 495 400‑50‑50
    </a>
  </div>

  <button class="hamburger hamburger--squeeze" type="button" aria-label="Открыть меню">
    <span class="hamburger-box">
      <span class="hamburger-inner"></span>
    </span>
  </button>
</header>
.header .hamburger {
  grid-column: 4;
  grid-row: 1;
  color: inherit; /* Чтобы иконка унаследовала цвет текста, а не была белой */
  line-height: 0; /* Чтобы иконка не увеличивала высоту меню */
  padding: 0;
}

.header .hamburger-inner,
.header .hamburger-inner::before,
.header .hamburger-inner::after {
  border-radius: 0; /* Убираем скругления в гамбургере */
}

«Гамбургер» и его анимацию можно было бы сверстать вручную или заменить на иконки

Научим кнопку «кли­каться». Пус­кай пока она меняет своё состо­я­ние и добав­ляет или уби­рает класс у меню:

<script>
  document
    .querySelectorAll('.hamburger') // Находим кнопку-гамбургер
    .forEach(button => {
     const nav = button
      .closest('.header')
      .querySelector('.nav') // Находим ближайшее к кнопке меню

     button.addEventListener('click', () => { // По клику на кнопку добавляем или удаляем класс is-active у кнопки и меню
       button
         .classList
         .toggle('is-active')

       nav
        .classList
        .toggle('is-active')
     })
  })
</script>

Внутри <script> пишут текст на Яваскрипте — языке программирования, встроенном в браузеры. Яваскрипт используют для создания интерактивных страниц и веб‑приложений

Под­клю­чим меню к кнопке и доба­вим ани­ма­цию. Пусть меню при­ез­жает сверху:

 @media (width > 690px) {
   .header .hamburger {
     display: none;
   }
 }

 @media (width <= 690px) {
   .header {
     position: relative;
     grid-template-columns: min-content 1fr 1fr min-content;
   }
   
   .header .hamburger {
     grid-column: 4;
     grid-row: 1;
   }
   
   .header .tel {
     grid-column: 2 / span 2;
     grid-row: 1;
     justify-self: stretch;
   }

   .nav {
     position: absolute;
     top: 0;
     left: 0;
     width: 100%;
     padding: 65px 10px 20px;
     transform: translateY(-100%); /* Прячем меню за верхнюю границу экрана */
     transition: transform 75ms ease; /* Анимируем изменение позиции меню, синхронизировав с анимацией кнопки */
     transition-delay: 120ms;
   }

   .nav.is-active {
     transform: translateY(0); /* Когда меню активно, ставим его под шапку */
     transition-timing-function: cubic-bezier(.215, .61, .355, 1);
   }

   .nav ul {
     grid-auto-flow: row;
     grid-auto-rows: max-content;
     row-gap: 10px;
   }
}

Тайминги и скорость анимации скопировали из кнопки, чтобы не было рассинхрона

Появи­лась про­блема с напол­за­нием. Выез­жа­ю­щее меню пере­кры­вает лого­тип. Попра­вим это, доба­вив слой, раз­де­ля­ю­щий выез­жа­ю­щее меню и лого­тип с кнопкой:

 @media (width > 690px) {
   .header .hamburger {
     display: none;
   }
 }

 @media (width <= 690px) {
   .header {
     position: relative;
     grid-template-columns: min-content 1fr 1fr min-content;
   }
   
   .header .hamburger {
     grid-column: 4;
     grid-row: 1;
   }
   
   .header .tel {
     grid-column: 2 / span 2;
     grid-row: 1;
     justify-self: stretch;
   }

   .nav {
     position: absolute;
     top: 0;
     left: 0;
     width: 100%;
     padding: 65px 10px 20px;
     transform: translateY(calc(60px - 100%)); /* Спрячем меню под слой-разделитель */
     z-index: 1; /* Меню поедет на самом нижнем уровне */
     transition: transform 75ms ease;
     transition-delay: 120ms;
   }
  
  .nav.is-active {
    transform: translateY(0);
    transition-timing-function: cubic-bezier(.215, .61, .355, 1);
  }
  
  .nav ul {
     grid-auto-flow: row;
     grid-auto-rows: max-content;
     row-gap: 10px;
   }

  .header::before {
    content: '';
    background: inherit;
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 2; /* Слой-разделитель скроет меню, пока оно проезжает под шапкой */
  }

  .header .logo,
  .header .tel,
  .header .hamburger {
    position: relative;
    z-index: 3; /* Логотип, телефон и кнопка встанут на самом верху */
  }
}

Адаптируем шапку к мобильной версии с помощью гамбургерного меню. Будет скрывать меню и показывать «гамбургер» при той ширине экрана, при которой меню не помещается в него целиком.

Возьмём шапку без мобильной версии и добавим кнопку гамбургер:

<header class="header">
  <div class="logo">
    <a href="/">
      <img src="logo.svg" width="40" height="40">
    </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>
      <li class="nav__item">
        <a href="tel:+74954005050">+7 495 400‑50‑50</a>
      </li>
    </ul>
  </nav>
  
  <div class="tel">
    <a href="tel:+74954005050">
      +7 495 400‑50‑50
    </a>
  </div>

  <!-- aria-label подскажет смысл кнопки программам чтения с экрана -->
  <button class="hamburger" type="button" aria-label="Открыть меню">
    <span class="hamburger-box">
      <span class="hamburger-inner"></span>
    </span>
  </button>
</header>

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

@media (width > 690px) {
  .header .hamburger {
    display: none;
  }
}

@media (width <= 690px) {
  .header {
    position: relative; /* Задаём контекст для позиционирования меню */
    grid-template-columns: min-content 1fr 1fr min-content; /* Определяем четыре колонки: первую и последнюю — по ширине логотипа и кнопки, вторую и третью — 50% от оставшегося места */
  }

  .header .hamburger {
    grid-column: 4; /* Кнопку ставим в последнюю колонку */
  }

  .header .tel {
    grid-column: 2 / span 2; /* Телефону отдаём две центральные колонки */
    justify-self: stretch;
  }

  .nav {
    position: absolute; /* Меню позиционируем абсолютно, сразу после шапки поверх содержимого. Из-за этого кнопка перестанет нажиматься, исправим это на следующем шаге */
    top: 0;
    left: 0;
    width: 100%; /* Растягиваем меню на всю ширину экрана */
    padding: 65px 10px 20px; /* Добавляем настолько большой верхний падинг, чтобы меню встало точно под шапкой */
  }

  .nav ul {
    grid-auto-flow: row; /* Раскладываем меню рядами */
    grid-auto-rows: max-content;
    row-gap: 10px;
  }
}

Стилизуем кнопку. Для простоты возьмём готовые стили и анимацию из библиотеки «гамбургерных» иконок на ЦСС:

<link rel="stylesheet" src="styles/hamburgers.css">

<header class="header">
  <div class="logo">
    <a href="/">
      <img src="logo.svg" width="40" height="40">
    </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>
      <li class="nav__item">
        <a href="tel:+74954005050">+7 495 400‑50‑50</a>
      </li>
    </ul>
  </nav>
  
  <div class="tel">
    <a href="tel:+74954005050">
      +7 495 400‑50‑50
    </a>
  </div>

  <button class="hamburger hamburger--squeeze" type="button" aria-label="Открыть меню">
    <span class="hamburger-box">
      <span class="hamburger-inner"></span>
    </span>
  </button>
</header>
.header .hamburger {
  grid-column: 4;
  grid-row: 1;
  color: inherit; /* Чтобы иконка унаследовала цвет текста, а не была белой */
  line-height: 0; /* Чтобы иконка не увеличивала высоту меню */
  padding: 0;
}

.header .hamburger-inner,
.header .hamburger-inner::before,
.header .hamburger-inner::after {
  border-radius: 0; /* Убираем скругления в гамбургере */
}

«Гамбургер» и его анимацию можно было бы сверстать вручную или заменить на иконки

Научим кнопку «кликаться». Пускай пока она меняет своё состояние и добавляет или убирает класс у меню:

<script>
  document
    .querySelectorAll('.hamburger') // Находим кнопку-гамбургер
    .forEach(button => {
     const nav = button
      .closest('.header')
      .querySelector('.nav') // Находим ближайшее к кнопке меню

     button.addEventListener('click', () => { // По клику на кнопку добавляем или удаляем класс is-active у кнопки и меню
       button
         .classList
         .toggle('is-active')

       nav
        .classList
        .toggle('is-active')
     })
  })
</script>

Внутри <script> пишут текст на Яваскрипте — языке программирования, встроенном в браузеры. Яваскрипт используют для создания интерактивных страниц и веб‑приложений

Подключим меню к кнопке и добавим анимацию. Пусть меню приезжает сверху:

 @media (width > 690px) {
   .header .hamburger {
     display: none;
   }
 }

 @media (width <= 690px) {
   .header {
     position: relative;
     grid-template-columns: min-content 1fr 1fr min-content;
   }
   
   .header .hamburger {
     grid-column: 4;
     grid-row: 1;
   }
   
   .header .tel {
     grid-column: 2 / span 2;
     grid-row: 1;
     justify-self: stretch;
   }

   .nav {
     position: absolute;
     top: 0;
     left: 0;
     width: 100%;
     padding: 65px 10px 20px;
     transform: translateY(-100%); /* Прячем меню за верхнюю границу экрана */
     transition: transform 75ms ease; /* Анимируем изменение позиции меню, синхронизировав с анимацией кнопки */
     transition-delay: 120ms;
   }

   .nav.is-active {
     transform: translateY(0); /* Когда меню активно, ставим его под шапку */
     transition-timing-function: cubic-bezier(.215, .61, .355, 1);
   }

   .nav ul {
     grid-auto-flow: row;
     grid-auto-rows: max-content;
     row-gap: 10px;
   }
}

Тайминги и скорость анимации скопировали из кнопки, чтобы не было рассинхрона

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

 @media (width > 690px) {
   .header .hamburger {
     display: none;
   }
 }

 @media (width <= 690px) {
   .header {
     position: relative;
     grid-template-columns: min-content 1fr 1fr min-content;
   }
   
   .header .hamburger {
     grid-column: 4;
     grid-row: 1;
   }
   
   .header .tel {
     grid-column: 2 / span 2;
     grid-row: 1;
     justify-self: stretch;
   }

   .nav {
     position: absolute;
     top: 0;
     left: 0;
     width: 100%;
     padding: 65px 10px 20px;
     transform: translateY(calc(60px - 100%)); /* Спрячем меню под слой-разделитель */
     z-index: 1; /* Меню поедет на самом нижнем уровне */
     transition: transform 75ms ease;
     transition-delay: 120ms;
   }
  
  .nav.is-active {
    transform: translateY(0);
    transition-timing-function: cubic-bezier(.215, .61, .355, 1);
  }
  
  .nav ul {
     grid-auto-flow: row;
     grid-auto-rows: max-content;
     row-gap: 10px;
   }

  .header::before {
    content: '';
    background: inherit;
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 2; /* Слой-разделитель скроет меню, пока оно проезжает под шапкой */
  }

  .header .logo,
  .header .tel,
  .header .hamburger {
    position: relative;
    z-index: 3; /* Логотип, телефон и кнопка встанут на самом верху */
  }
}
Скрыто 45 разворотов