Адаптивное меню с поддержкой retina

Original author: Stéphanie Walter
  • Translation
  • Tutorial
В этой статье очень подробно описано пошаговое создание адаптивного меню для сайта с несколькими вариантами компоновки элементов (в зависимости от размера экрана девайса). Для поддержки retina-экранов используется иконочный шрифт.



Демо / Скачать исходники


Подготовка иконочного шрифта


Создание иконочного шрифта вручную может доставить проблем, но к счастью, имеются веб-сервисы, которые облегчают такие задачи разработчику. IcoMoon — один из таких инструментов.

В первую очередь следует подготовить SVG-файлы иконок и импортировать их в IcoMoon:



На сайте уже есть множество готовых файлов для создания шрифта, можно частично использовать их. Каждой иконке можно назначить клавишу:



На выходе получается ZIP-файл со шрифтами в четырех форматах: SVG, EOT, TTF и WOFF, а также CSS файл и демо-страница. Кстати, чтобы шрифт лучше смотрелся в Chrome, можно использовать специальных хак.

HTML


Разметка нашего меню:

<nav  id="menu" class="nav">  
    <ul>
        <li>
            <a  href="#" title="">
                <span  class="icon"> <i aria-hidden="true"  class="icon-home"></i></span><span>Home</span>
            </a>
        </li>
        <li>      
            <a href="#" title=""><span class="icon"> <i aria-hidden="true" class="icon-services"></i></span><span>Services</span></a>   
        </li> 
        <li>
            <a  href="#" title=""><span  class="icon"><i  aria-hidden="true" class="icon-portfolio"></i></span><span>Portfolio</span></a>
        </li>
        <li>
            <a  href="#" title=""><span  class="icon"><i  aria-hidden="true" class="icon-blog"></i></span><span>Blog</span></a> 
        </li>
        <li>
            <a  href="#" title=""><span  class="icon"><i  aria-hidden="true" class="icon-team"></i></span><span>The  team</span></a>    
        </li>
        <li>
            <a  href="#" title=""><span  class="icon"><i  aria-hidden="true" class="icon-contact"></i></span><span>Contact</span></a>
        </li>
    </ul>
</nav>


Для добавления иконочного шрифта используем CSS-класс «icon-iconname» внутри тега i. Также добавим класс «no-js» в body, который будет меняться на класс «js» с помощью Modernizr. Это нужно для того, чтобы меню корректно работало у пользователей с отключенным JavaScript.

CSS и JavaScript


Основной CSS для всех типов экранов:

.nav ul {
    max-width: 1240px;
    margin: 0;
    padding: 0;
    list-style: none;
    font-size: 1.5em;
    font-weight: 300;
}
 
.nav li span {
    display: block;
}
 
.nav a {
    display: block;
    color: rgba(249, 249, 249, .9);
    text-decoration: none;
    transition: color .5s, background .5s, height .5s;
}
 
.nav i{
    /* сглаживание шрифта для Chrome */
    transform: translate3d(0, 0, 0);
}
 
/* Убирает синий Webkit-фон при тапе на тач-скрине */
 
a, button {
    -webkit-tap-highlight-color: rgba(0,0,0,0);
}


Основной hover-эффект:

.no-touch .nav ul:hover a {
    color: rgba(249, 249, 249, .5);
}
 
.no-touch .nav ul:hover a:hover {
    color: rgba(249, 249, 249, 0.99);
}


Добавляем фоновый цвет для элементов меню. Благодаря использованию nth-child можно добавлять сколько угодно элементов списка, цвет будет наследоваться:

.nav li:nth-child(6n+1) {
    background: rgb(208, 101, 3);
}
 
.nav li:nth-child(6n+2) {
    background: rgb(233, 147, 26);
}
 
.nav li:nth-child(6n+3) {
    background: rgb(22, 145, 190);
}
 
.nav li:nth-child(6n+4) {
    background: rgb(22, 107, 162);
}
 
.nav li:nth-child(6n+5) {
    background: rgb(27, 54, 71);
}
 
.nav li:nth-child(6n+6) {
    background: rgb(21, 40, 54);
}


Используем media query для изменения формы меню на меньшем экране:

@media (min-width: 50em) {
 
    /* Transforms the list into a horizontal navigation */
    .nav li {
        float: left;
        width: 16.66666666666667%;
        text-align: center;
        transition: border .5s;
    }
 
    .nav a {
        display: block;
        width: auto;
    }


Добавляем border разных цветов для элементов меню:

.no-touch .nav li:nth-child(6n+1) a:hover,
.no-touch .nav li:nth-child(6n+1) a:active,
.no-touch .nav li:nth-child(6n+1) a:focus {
    border-bottom: 4px solid rgb(174, 78, 1);
}
 
.no-touch .nav li:nth-child(6n+2) a:hover,
.no-touch .nav li:nth-child(6n+2) a:active,
.no-touch .nav li:nth-child(6n+2) a:focus {
    border-bottom: 4px solid rgb(191, 117, 20);
}
 
.no-touch .nav li:nth-child(6n+3) a:hover,
.no-touch .nav li:nth-child(6n+3) a:active,
.no-touch .nav li:nth-child(6n+3) a:focus {
    border-bottom: 4px solid rgb(12, 110, 149);
}
 
.no-touch .nav li:nth-child(6n+4) a:hover,
.no-touch .nav li:nth-child(6n+4) a:active,
.no-touch .nav li:nth-child(6n+4) a:focus {
    border-bottom: 4px solid rgb(10, 75, 117);
}
 
.no-touch .nav li:nth-child(6n+5) a:hover,
.no-touch .nav li:nth-child(6n+5) a:active,
.no-touch .nav li:nth-child(6n+5) a:focus {
    border-bottom: 4px solid rgb(16, 34, 44);
}
 
.no-touch .nav li:nth-child(6n+6) a:hover,
.no-touch .nav li:nth-child(6n+6) a:active,
.no-touch .nav li:nth-child(6n+6) a:focus {
    border-bottom: 4px solid rgb(9, 18, 25);
}


Стили иконок и текста в нашем меню:

.icon {
    padding-top: 1.4em;
}
 
.icon + span {
    margin-top: 2.1em;
    transition: margin .5s;
}


Небольшая анимация меню:

/* Пункт меню увеличивается по высоте*/
.nav a {
    height: 9em;
}
 
.no-touch .nav a:hover ,
.no-touch .nav a:active ,
.no-touch .nav a:focus {
    height: 10em;
}   
 
/* Движение текста */
.no-touch .nav a:hover .icon + span {
    margin-top: 3.2em;
    transition: margin .5s;
}


CSS transition для иконок:

.nav i {
    position: relative;
    display: inline-block;
    margin: 0 auto;
    padding: 0.4em;
    border-radius: 50%;
    font-size: 1.8em;
    box-shadow: 0 0 0 0.8em transparent;
    background: rgba(255,255,255,0.1);
    transform: translate3d(0, 0, 0);
    transition: box-shadow .6s ease-in-out;
}   


Для нужного визуального эффекта меняем тень элемента:

    .no-touch .nav a:hover i,
    .no-touch .nav a:active i,
    .no-touch .nav a:focus i {      
        box-shadow: 0 0 0px 0px rgba(255,255,255,0.2);
        transition: box-shadow .4s ease-in-out;
    }
         
}


Еще один media query, для экранов от 800 до 980 пикселей:

@media (min-width: 50em) and (max-width: 61.250em) {
 
    /* Size and font adjustments to make it fit better */
    .nav ul {
        font-size: 1.2em;
    }
 
}


Закончили с CSS для «desktop» версии меню, переходим к «tablet» и «mobile»:

/*  "tablet" и "mobile" версии*/
 
@media (max-width: 49.938em) {      
     
    /* Вместо добавления border модифицируем цвет фона */
    .no-touch .nav ul li:nth-child(6n+1) a:hover,
    .no-touch .nav ul li:nth-child(6n+1) a:active,
    .no-touch .nav ul li:nth-child(6n+1) a:focus {
        background: rgb(227, 119, 20);
    }
 
    .no-touch .nav li:nth-child(6n+2) a:hover,
    .no-touch .nav li:nth-child(6n+2) a:active,
    .no-touch .nav li:nth-child(6n+2) a:focus {
        background: rgb(245, 160, 41);
    }
 
    .no-touch .nav li:nth-child(6n+3) a:hover,
    .no-touch .nav li:nth-child(6n+3) a:active,
    .no-touch .nav li:nth-child(6n+3) a:focus {
        background: rgb(44, 168, 219);
    }
 
    .no-touch .nav li:nth-child(6n+4) a:hover,
    .no-touch .nav li:nth-child(6n+4) a:active,
    .no-touch .nav li:nth-child(6n+4) a:focus {
        background: rgb(31, 120, 176);
    }
 
    .no-touch .nav li:nth-child(6n+5) a:hover,
    .no-touch .nav li:nth-child(6n+5) a:active,
    .no-touch .nav li:nth-child(6n+5) a:focus {
        background: rgb(39, 70, 90);
    }
 
    .no-touch .nav li:nth-child(6n+6) a:hover,
    .no-touch .nav li:nth-child(6n+6) a:active,
    .no-touch .nav li:nth-child(6n+6) a:focus {
        background: rgb(32, 54, 68);
    }
 
    .nav ul li {
        transition: background 0.5s;
    }   
 
}


Для экранов размером от 520px (32.5em) до 799px (49.938em) меню должно отображаться в две колонки. Добавляем немного отступов для удобного тапа на тач-скринах:

 
@media (min-width: 32.5em) and (max-width: 49.938em) {
     
    /* Делаем две колонки */
    .nav li {
        display: block;
        float: left;
        width: 50%;
    }
     
    /* Добавляем отступ */
    .nav a {
        padding: 0.8em;     
    }
 
    /* Перемещаем иконки и текст в левую часть элементов меню */
    .nav li span, 
    .nav li span.icon {
        display: inline-block;
    }
 
    .nav li span.icon {
        width: 50%;
    }
 
    .nav li .icon + span {
        font-size: 1em;
    }
 
    .icon + span {
        position: relative;
        top: -0.2em;
    }


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

    .nav li i {
        display: inline-block;
        padding: 8% 9%;
        border: 4px solid transparent;
        border-radius: 50%;
        font-size: 1.5em;
        background: rgba(255,255,255,0.1);
        transition: border .5s;
    }

    .no-touch .nav li:hover i,
    .no-touch .nav li:active i,
    .no-touch .nav li:focus i {
        border: 4px solid rgba(255,255,255,0.1);
    }
 
}


Адаптируем размер шрифта и ширину:

@media (min-width: 32.5em) and (max-width: 38.688em) {
     
    .nav li span.icon {
        width: 50%;
    }
 
    .nav li .icon + span {
        font-size: 0.9em;
    }
}


Для самых маленьких экранов необходимо скрыть всю навигацию, оставив только кнопку «Menu», по клику на которую открываются все пункты. Используем JavaScript:

//  функция для смены класса
var changeClass = function (r,className1,className2) {
    var regex = new RegExp("(?:^|\\s+)" + className1 + "(?:\\s+|$)");
    if( regex.test(r.className) ) {
        r.className = r.className.replace(regex,' '+className2+' ');
    }
    else{
        r.className = r.className.replace(new RegExp("(?:^|\\s+)" + className2 + "(?:\\s+|$)"),' '+className1+' ');
    }
    return r.className;
};  
 
//  кнопка для маленьких экранов
var menuElements = document.getElementById('menu');
menuElements.insertAdjacentHTML('afterBegin','<button type="button" id="menutoggle" class="navtoogle" aria-hidden="true"><i aria-hidden="true" class="icon-menu"> </i> Menu</button>');
 
//  меняем класс для скрытия/отображения меню
document.getElementById('menutoggle').onclick = function() {
    changeClass(this, 'navtoogle active', 'navtoogle');
}
 
// document click для скрытия меню
// http://tympanus.net/codrops/2013/05/08/responsive-retina-ready-menu/comment-page-2/#comment-438918
document.onclick = function(e) {
    var mobileButton = document.getElementById('menutoggle'),
        buttonStyle =  mobileButton.currentStyle ? mobileButton.currentStyle.display : getComputedStyle(mobileButton, null).display;
 
    if(buttonStyle === 'block' && e.target !== mobileButton && new RegExp(' ' + 'active' + ' ').test(' ' + mobileButton.className + ' ')) {
        changeClass(mobileButton, 'navtoogle active', 'navtoogle');
    }
}


Для более чистого HTML-кода кнопка «Menu» создается непосредственно в JavaScript. CSS для кнопки:

.nav .navtoogle{
    display: none;  
    width: 100%;
    padding: 0.5em 0.5em 0.8em;
    font-family: 'Lato',Calibri,Arial,sans-serif;
    font-weight: normal;
    text-align: left;
    color: rgb(7, 16, 15);
    font-size: 1.2em;
    background: none;   
    border: none;
    border-bottom: 4px solid rgb(221, 221, 221);
    cursor: pointer;
}
 
.navtoogle i{
    z-index:-1;
}
 
.icon-menu {
    position: relative;
    top: 3px;
    line-height: 0;
    font-size: 1.6em;
}


По умолчанию кнопка скрыта, появляется при размере экрана менее 519px (32.438em):

@media (max-width: 32.438em) {
 
    .nav .navtoogle{
        margin: 0;
        display: block;
    }


Используем класс «no-js» для отображения меню у пользователей без поддержки JavaScript:

.no-js .nav ul {
    max-height: 30em;
    overflow: hidden;
}


Когда JavaScript поддерживается, скрываем меню:

.js .nav ul {
    max-height: 0em;
    overflow: hidden;
}
 
/* Отображение меню по клику на кнопку */
.js .nav .active + ul {     
    max-height: 30em;
    overflow: hidden;
    transition: max-height .4s;
}


Адаптируем меню для самых маленьких экранов:

.nav li span {
    display: inline-block;
    height: 100%;
}
 
.nav a {
    padding: 0.5em;     
}
 
.icon + span {
    margin-left: 1em;
    font-size: 0.8em;
}


Добавляем border слева:

.nav li:nth-child(6n+1) {
    border-left: 8px solid rgb(174, 78, 1);
}
 
.nav li:nth-child(6n+2) {
    border-left: 8px solid rgb(191, 117, 20);
}
 
.nav li:nth-child(6n+3) {
    border-left: 8px solid rgb(13, 111, 150);
}
 
.nav li:nth-child(6n+4) {
    border-left: 8px solid rgb(10, 75, 117);
}
 
.nav li:nth-child(6n+5) {
    border-left: 8px solid rgb(16, 34, 44);
}
 
.nav li:nth-child(6n+6) {
    border-left: 8px solid rgb(9, 18, 25);
}


Получившаяся навигация выглядит хорошо в браузере десктопа при изменении размеров окна, однако на мобильных устройствах могут возникать проблемы с тачем пунктов меню. Чтобы определить тач-устройство, используем Modernizr. Если тач поддерживается, добавляется тег «touch»:

 .touch .nav a {
        padding: 0.8em;
    }
}


Вот и все, получилась красивая retina-ready навигация, хорошо отображающаяся на любых устройствах!
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 18

    +3
    А можно вопрос не по теме: чем так хорош адаптивный дизайн по сравнению с хорошей и грамотной резиной? Все эти перекраивания интерфейса для разных размеров — у меня, например, явная с ними проблема: невозможно на телефоне найти то, что знаешь, как найти на десктопном сайте, ибо либо все в разные места уезжает, либо (и это хуже) еще и в какие-то отдельные подменю. В самом запущенном случае функционал вообще урезается.

    Неужели кому-то это нужно (учится работать сразу в нескольких интерфейсах и запоминать, что где в разных «версиях» сайтов)? Всегда ищу «показать десктопную версию» и крайне негодую, если ее принудительно скрывают. При этом я не имею ничего против нативных приложений.
      +4
      На некоторые элементы, как, например, в шапке этой статьи адаптивный дизайн ложится просто прекрасно. Но в целом я согласен, сверстать что-то сложнее небольшого корпоративного сайта, особенно если там присутствует контент создаваемый пользователями — лучше сразу застрелиться.

      Да, я знаю что это очень непопулярное мнение в наше время, но я честно пытался применить адаптивную верстку для средней величины проекта, который писали-верстали с нуля. Если интересно, могу рассказать про подводные камни, до которых мы «доплыли».
        0
        было бы интересно.
          +3
          Первое, с чем я вообще раньше не сталкивался, это проблемы с баннерами, которые в обязательном порядке должны были занимать определенные места на сайте (справа от основного контента на странице). На этот счет был подписан договор еще до начала работы, и, естественно, никаких вариантов с тем, что реклама будут перемещаться вместе с контентом там не было.

          Второе, менеджерское, это то, что гибкий процесс разработки средних и крупных проектов предполагает постоянное добавление каких-то новых, не запланированных изначально фич. Чем идеальнее получилась верстка какого-то раздела, тем больше шанс, что туда нужно будет кровь из носу добавить что-то еще. Соответственно время на разработку это новой фичи увеличится, минимум, вдвое, по количеству макетов. А то и вообще эту фичу «забудут» добавить в адаптивный вариант, ну знаете, как это бывает, давайте пока добавим, посмотрим, а потом… Конечно это потом никогда не настает.

          Третье, чисто дизайнерское — некоторые элементы очень тесно связанны логически и взрывают пользователю мозг, если они находятся не на своих привычных местах. Ну, самый простой пример — например если прайс и краткое описание всегда идут справа от фотографии, то подробности — снизу. И хоть убей их расположение должно быть именно таким, любые его изменения делают юзабили хуже. А если это и есть 80% контента сайта, то вся «адаптивность» сводится к адаптации и верхнего меню и блока с копирайтом, с которыми, в общем-то, и так все хорошо, даже на мобильных телефонах.

          Четвертое — многоколоночная верстка далеко не всегда предполагает четкую иерархию материала — вы не можете просто взять и поставить что-то выше, а что-то ниже.

          Как-то так.

            0
            Спасибо за подробный ответ. Хотя я, как верстальщик-фронтендщик, не увидел тут проблем собственно адаптивной верстки. У вас больше специфика проекта. И причина этих проблем, как мне видится — отсутствие понятного языка дизайна. Это общая проблема мобильных сайтов. Лучшее известное мне решение — это Twitter Bootstrap.

            А то и вообще эту фичу «забудут» добавить в адаптивный вариант, ну знаете, как это бывает, давайте пока добавим, посмотрим, а потом… Конечно это потом никогда не настает.

            Я подобные ситуации решаю грамотной организацией модулей кода и использую прекомпиляторы. Это вопрос рабочей дисциплины.

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

            Не верю ©. Звучит как прихоть дизайнера.
        +3
        А можете привести пример грамотной резины?
      • UFO just landed and posted this here
          +2
          Так и отлично. Ничего же не потерялось (имею ввиду функционал).
          0
          На сайте с демо еще много красивых примеров — некоторые можно взять на вооружение.
            0
            Кстати, 8кб модернайзера в плагине можно обменять на две строчки.
              +2
              Будте любезны, приведите эти две строчки.
                0
                $('body').removeClass('no-js');
                if ('ontouchstart' in document.documentElement) { $('body').addClass('touch'); }
                
                  +4
                  Но там же может отсутствовать джейквери, infinius!
                  document.getElementsByTagName('body')[0].className.replace(/\bno-js\b/g, '');
                  if ('ontouchstart' in document.documentElement) { document.getElementsByTagName('body')[0].className += ' touch';  }
                  
                    0
                    Как всё таки правильнее, in document.documentElement как в примере выше, или
                    (('ontouchstart' in window) || (navigator.msMaxTouchPoints > 0))?
                      0
                      navigator.msMaxTouchPoints или 'onmsgesturechange' in window определит ещё и виндоусовские браузеры, ваш вариант лучше.
                    0
                    var hasTouchSupport = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch;
                    var bodyClass = "js " + (hasTouchSupport ? "touch" : "no-touch");
                    document.body.className = bodyClass;
                    

                    В самом modernizr используется версия с дополнительной проверкой с помощью CSS
                      0
                      Последняя строка затрёт классы у body. Нужно как-то так, но появляется третья строка. Не по-мужски, нужно убрать перенос где-то.
                      var classes = document.body.className.replace(/\bno-js\b/g, 'js');
                      var isTouch = (('ontouchstart' in window) || ('onmsgesturechange' in window));
                      document.body.className = classes + (isTouch ? " touch" : " no-touch");
                      
                  0
                  мне кажется не надо так много когда вываливать, его читать все равно нет смысла весь, достаточно описать словами как вы делаете конкретную фичу а потом фрагмент конкретного места кода для примера.

                  Only users with full accounts can post comments. Log in, please.