Кастомизация скроллбаров в браузере: компромисс между технологиями html, css, js и удобством использования

Статья посвещена решению проблемы кастомизации скроллбаров браузера ради воплощения в жизнь амбициозных идей дизайнера. Статья расчитана на тех, кто свободно ориентируется в технологиях html, css, js, т.к. предлагаемое решение основано на их компромиссном использовании.

Кастомизация скроллбаров в браузере: компромисс между технологиями html, css, js и удобством использования

В статье будут описаны и решены следующие задачи и цели:

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

Intro. Возможности системного скрола.

Чтобы было от чего отталкиваться, я приведу простой пример (посмотреть в работе):

<div style="border: 3px solid #f08080; height: 300px; overflow: auto; width: 300px;">
    <div style="background-color: #90ee90; border: 10px solid #4169e1; min-height: 600px; min-width: 600px;">
        content...
    </div>
</div>

Браузер для навигации по контенту в такой области предоставляет следующие возможности (советую попробовать все из них):

  • колесо мыши (некоторые мыши кроме вертикальной прокрутки предоставляют возможность горизонтальной прокрутки)
  • клавиши-стрелки при фокусе на элементе (вверх, вниз, вправо, влево)
  • тачпад (особенные удобства и кайф от манипулирования контентом можно ощутить на тачпадах у компьютеров Macintosh)
  • тачскрин планшетов и телефонов, а также и компьютеров
  • также можно прокрутить контент, выделяя его (такая возможность предусмотрена для выделения, например, больших текстов, часть которых находится за пределами видимости)
  • и, непосредственно, элементы скроллбаров: ползунки и кнопки

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

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

Итак. 

Решение имеет следующую структуру на html:

<div class="scrollar">
    <div class="scrollar-viewport">
        <div class="scrollar-systemscrolls">
            <div class="scrollar-contentwrap">
                <div class="scrollar-content"></div>
            </div>
        </div>
    </div>

    <div class="scrollar-scroll scrollar-vscroll">
        <div class="scrollar-btn scrollar-btnup"></div>
        <div class="scrollar-btn scrollar-btndown"></div>
        <div class="scrollar-track scrollar-vtrack">
            <div class="scrollar-thumb scrollar-vthumb"></div>
        </div>
    </div>

    <div class="scrollar-scroll scrollar-hscroll">
        <div class="scrollar-btn scrollar-btnleft"></div>
        <div class="scrollar-btn scrollar-btnright"></div>
        <div class="scrollar-track scrollar-htrack">
            <div class="scrollar-thumb scrollar-hthumb"></div>
        </div>
    </div>

    <div class="scrollar-corner"></div>
</div>

Scrollar — одновременно название решения и namespace, чтобы исключить пересечение стилей на целевой странице, где решение может быть использовано.

Кастомизация скроллбаров в браузере: компромисс между технологиями html, css, js и удобством использования

Чтобы было понятнее — поясню структуру:

  • корневой блок является оберткой для всей конструкции и определяет область и размеры всей конструкции в целом;
  • viewport — этим блоком ограничена область просмотра контента;
  • systemscrolls — этот блок отвечает за прокрутку контента;
  • contentwrap — это корректирующий блок (о его смысле и возможностях ниже);
  • content — непосредственно сам контент;
  • vtrack и htrack — вертикальный и горизонтальный скроллбары. С их содержимым и так все понятно.

А теперь можно приступить к рассмотрению ключевых моментов…

Основная тяжесть: операции Scroll и Resize

На мой взгляд, это самые «тяжелые» и неудобные в реализации на javascript операции. Почему? Чтобы программно реализовать Resize и сохранение пропорций нужно, в основном, обрабатывать событие window.onresize, а во время возникновения этого события — корректировать размеры и пропорции у нескольких элементов (чаще всего так…). Недостатком ресайза подобным образом является неплавное (с небольшими непостоянными, заметными глазу, шагами) изменение размеров элемента, кто пытался подобное отладить — поймет меня. Это сильно «напрягает» глаза, когда вкладываешь душу в разработку и стараешься довести работу всего интерфейса до идела.

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

.scrollar, .scrollar-viewport, .scrollar-systemscrolls, .scrollar-contentwrap, .scrollar-content {
    bottom: 0px;
    height: auto;
    left: 0px;
    position: absolute;
    right: 0px;
    top: 0px;
    width: auto;
}

/* корректировка стиля корневого элемента */
.scrollar { overflow: visible; position: relative; }

/* эти блоки не должны иметь привязки справа и снизу */
.scrollar-contentwrap, .scrollar-content {
    bottom: auto;
    right: auto;
}

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

О Scroll. Для реализации scroll на desktop-браузерах необходимо обрабатывать событие колеса мыши и анализировать значения от этого события (также не забывать, что некоторые мыши позволяют листать контент в горизонтальной плоскости, а не только в вертикальной), а для mobile-браузеров нужно обрабатывать события группы touch. Т.е. для кроссплатформенного решения нужно программировать две эти реализации. Но лучше прокрутку контента возложить на сам браузер. Достаточно определить стиль для элемента systemscrolls:

.scrollar-systemscrolls { overflow: scroll; }

Скрытие системных скроллбаров и 22px

Решая задачу прокрутки контента, я использовал свойство overflow:scroll, которое заставляет браузер отображать скроллбары всегда и тем самым предоставляет пользователю все удобства системной прокрутки. Но теперь нужно эти скроллбары скрыть. Здесь как раз и выручит viewport — этот блок будет скрывать всё, что выходит за его пределы. Это можно сделать двумя способами:

.scrollar-viewport { overflow: scroll; }
.scrollar-viewport { clip: rect(0, auto, auto, 0); clip: rect(0 100% 100% 0); }

Первый вариант с overflow прост для понимания, но когда пользователь захочет выделить контент и начнет тянуть курсор в нужную сторону, то он, вполне вероятно, увидит системные скроллбары, т.к. при таком действии они вылезут из-под скрываемой области. Вариант с clip таким не страдает, но в этом случае пришлось применить небольшой хак и для поддержки ie7. Но это ещё не всё… Блок systemscrolls имеет такие же размеры, как и блок viewport, т.е. системные скроллбары еще видны. Здесь и используется ключевой момент «22px» — это величина, на которую будет скорректирован блок systemscrolls. Дело в том, что толщина скроллбаров у популярных браузеров менее 21px. Сама корректировка выглядит так:

.scrollar-systemscrolls {
    bottom: -22px;
    right: -22px;
}

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

И что в итоге? Браузер сам изменяет и следит за размерами всего элемента, контент легко и плавно прокручивается всеми описанными выше способами, а системные скроллбары скрыты. Но если это оставить в таком виде, то часть контента справа и снизу отображаться не будет…

Блок contentwrap

Основное и главное назначение блока contentwrap — это сделать так, чтобы в блоке viewport можно было увидеть блок content полностью: от одного края до другого при разных способах прокрутки.
До этого момента javascript не требовался, но сейчас он пригодится для того, чтобы скорректировать размеры блока contentwrap.

…
var viewport = $(".scrollar-viewport", scrollar);
var systemscrolls = $(".scrollar-systemscrolls", scrollar);
var correct_h = systemscrolls[0].clientHeight - viewport.height(); // корректировка по высоте
var correct_w = systemscrolls[0].clientWidth - viewport.width(); // корректировка по ширине
…

таким образом, размеры элемента contentwrap будут получаться из сложения этих величин с размерами блока content, и делать это будет нужно при каждом изменении размеров блока content. Есть исключения, но о них будет рассказано ниже.

Корректировка блока contentwrap с помощью js позволяет не обращать внимания на вид и версию браузера и используемую им толщину скроллбаров.

Блоки vscroll и hscroll

vscroll и hscroll — скроллбары. На данный момент, основная задача — «приклеить» их к краям и заставить их изменять свои размеры и местоположение их дочерних элементов за счет браузера:

.scrollar-scroll,
.scrollar-track,
.scrollar-btn,
.scrollar-thumb,
.scrollar-corner {
    position: absolute;
}

.scrollar-hscroll {
    bottom: 0px;
    height: 0px;
    left: 0px;
    right: 0px;
}

.scrollar-vscroll {
    bottom: 0px;
    right: 0px;
    top: 0px;
    width: 0px;
}

.scrollar-btnup { left: 0px; top: 0px; }
.scrollar-btndown { bottom: 0px; left: 0px; }
.scrollar-btnleft { left: 0px; top: 0px; }
.scrollar-btnright { right: 0px; top: 0px; }

.scrollar-vthumb { max-height: 100%; height: 30px; left: 0px; right: 0px; }
.scrollar-hthumb { max-width: 100%; width: 30px; bottom: 0px; top: 0px; }

в этом листинге нет ничего сложного и я перейду к более интересной части: бегунки.

Бегунки

Для успешной реализации функционала бегунков нужно рассмотреть следующие задачи:

  • изменение положения бегунка при прокрутке контента указанными выше способами
  • перетаскивание бегунка указателем мыши и реакция контента на эти действия
  • изменение размеров и сохранение позиции бегунка при изменении размеров контента относительно размеров компонента или при изменении размеров компонента (эта задача решается при обновлении параметров всего компонента и решение будет описано ниже)
Изменение положения бегунка при прокрутке контента

Это сделать крайне просто. Благодаря установленному свойству overflow:scroll у блока systemscrolls можно ловить собитие scroll на этом блоке, а уже при возникновении этого события двигать бегунок в зависимости от положения (свойства scrollLeft и scrollTop) контента относительно левой верхней точки блока systemscrolls с учетом коэффициента пропорциональности, который вычисляется в функции обновления параметров компонента (об этом будет ниже).

Перетаскивание бегунков

Бегунок должен реагировать на поведение указателя мыши также, как и в системе. Сделать это весьма не сложно. Алгоритм заключается в следующем:

  • поймать событие mousedown на самом бегунке
  • подключить события mousemove и mouseup на элемент document
  • обрабатывать событие document.mousemove, тем самым изменяя положение бегунка и пролистывая контент
  • поймав событие document.mouseup — отключить события mousemove и mouseup на элемента document

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

  • событие scroll от элемента systemscrolls. Оно возникает следующим образом: чтобы пролистывать контент с помощью js, нужно изменять свойства scrollLeft и scrollTop у блока systemscrolls, а изменение этих свойств приведет к выбросу события scroll у этого блога. Таким образом сработает алгоритм по изменению положения бегунка при прокрутке контента, который описан выше.
  • после того, как будет поймано событие mousedown на бегунке, нужно отключить обработку события scroll на элементе systemscrolls, а элементы бегунков двигать по событию mousemove на элементе document одновременно пролистывая контент изменением свойств scrollLeft и scrollTop у блока systemscrolls.

На первый взгляд, первый способ кажется лучше, т.к. требует меньше усилий для реализации. Но недостатком этого способа является присутствующая обратная связь через событие systemscrolls.scroll (см. рисунок), из-за которой бегунок начинает заметно отставать от указателя мыши при быстром перемещении последнего. При этом, событие systemscrolls.scroll возникает всякий раз при изменении свойств scrollLeft и scrollTop у блока systemscrolls, вызывая функцию перемещения бегунков.
Кастомизация скроллбаров в браузере: компромисс между технологиями html, css, js и удобством использования

При втором способе можно получить гораздо лучший результат. Дополнительные операции по отсоединению и присоединению события systemscrolls.scroll происходят только два раза: на события mousemove и mouseup (соответственно) элемента document. Таким образом, обработка события document.mousemove происходить быстрее и оптимальнее (см. рисунок)
Кастомизация скроллбаров в браузере: компромисс между технологиями html, css, js и удобством использования

Обновление параметров компонента

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

// Горизонтальный скрол
        if (options.hscroll) {
            scroll = env.hscroll;

            var ss_cw = ss.clientWidth;

            // Корректировка ширины contentwrap
            cw.style.width = (ct.offsetWidth + env.correct_w) + "px";
            while (Math.abs(cw.scrollWidth - ct.offsetWidth) <= 1) {
                cw.style.width = (ct.offsetWidth + 1000) + "px";
                cw.style.width = (ct.offsetWidth + env.correct_w) + "px";
            }

            var ss_sw = ss.scrollWidth;

            // Ширина горизонтального ползунка
            var htrack_w = scroll.track.width();
            var hthumb_w = htrack_w * ss_cw / ss_sw;
            if (hthumb_w > htrack_w) {
                hthumb_w = htrack_w;
            } else if (hthumb_w < 30) {
                hthumb_w = 30;
            }
            scroll.thumb.outerWidth(hthumb_w);

            // Коэффициенты пропорциональности
            x = htrack_w - hthumb_w;
            scroll.ratio = (ss_sw - ss_cw) / (x < 1 ? 1 : x);
            if (scroll.ratio < 1) scroll.ratio = 1;

            // Крайние положение ползунка
            scroll.min_pos = 0;
            scroll.max_pos = htrack_w - hthumb_w;

            // Корректировка положения ползунка
            scroll.thumb.css("left", ss.scrollLeft / scroll.ratio);
        }

Из этого листинга хочу уделить внимание именно куску кода «Корректировка ширины contentwrap», остальное понятно и без объяснения. Смысл этого куска в следующем:

  • в браузере расширеннее контента по вертикали имеет приоритет перед горизонталью — это значит, что если менять ширину окна браузера (а контент не имеет каких-либо жестких ограничений по ширине), то высота контента будет увеличиваться без ограничений, а ширина уменьшаться. По сути всё логично и дружелюбно к пользователю, т.к. наличие у окна браузера горизонтального скрола одновременно с вертикальным — это определенные неудобства, то лучше вытягивать контент по вертикали. Но когда речь идет об области прокрутки какого-нибудь элемента интерфейса, то хотелось бы, чтобы область горизонтальной прокрутки увеличивалась автоматически, когда это нужно. И вот этот код из 5ти строчек решает эту задачу: определяет, когда браузер может выстроить элементы по горизонтали и тогда производит расширение контента до нужных размеров.

Когда нужно обновлять параметры компонента?

  • инициализация
  • изменение размеров окна браузера
  • изменение контанта

Если первые два случая весьма прозрачны, то вот последний таит в себе подводные камни. Контент может меняться не только во время удаления или добавления каких-либо элементов, но и по завершению загрузки каких-нибудь картинок, если их размеры не заданы заранее, и т.д. Можно отлавливать все варианты событий onload, но это не стоит того. Самое оптимальное решение — setInterval(update, 300) — функция update будет запускаться каждые 300 мс, нагрузки на браузер почти никакой и все весьма надежно.

Кастомизация

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

<div class="scrollar-scroll scrollar-vscroll">
        <div class="scrollar-btn scrollar-btnup"></div>
        <div class="scrollar-btn scrollar-btndown"></div>
        <div class="scrollar-track scrollar-vtrack">
            <div class="scrollar-thumb scrollar-vthumb"></div>
        </div>
    </div>

Эта верстка вертикального скроллбара (для горизонтального всё подобно). Преимущество такой структуры следующие:

  • обобщающие классы scrollar-scroll, scrollar-btn, scrollar-track и scrollar-thumb позволяют задавать общие стили как для вертикального, так и для горизонтального скроллбаров (например, задав scrollar-btn { display: none;} можно скрыть кнопки);
  • scrollar-btn и scrollar-track фиксируются относительно родительского элемента scrollar-scroll, что позволяет изменять их положение и их размеры, а также размеры scrollar-scroll, за счет браузера;
  • элемент scrollar-track не требует стилизации, т.к. он предназначен для определения области движения бегунка scrollar-thumb, но при желании можно и к этому элементу применить стили;
  • элементы позиционируются абсолютно (position: absolute) относительно scrollar-scroll, что позволяет размещать их относительно друг друга весьма легко;

По сути, вся кастомизация сводится к написанию стилей. В конце статьи будут приведены ссылки на примеры кастомизации, а также ссылки на файлы стилей для этих примеров.

А если горизонтальный или вертикальный скрол не нужен?

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

Как менять контент?

Есть функция content(«action», content), где action — функция jQuery по управлению содержимым (text, html, append, prepend…). Также эту функцию можно вызвать без параметров, тогда она вернет объект jQuery и с ним можно работать, в этом случае параметры компонента будут обновляться каждые 300 мс. Примеры:

  • content(«html», «Abcd») — заменит содержимое в области прокрутки на параграф с текстом, при этом обновление параметров компонента произойдет мгновенно, сразу по завершению функции;
  • content().html(«Abcd») — эффект будет таким же, как и в предыдущем примере, но обновление контента произойдет за счет setInterval, в крайнем случае можно принудительно вызвать функцию update()

Ссылки к статье:

Customization — на этой странице можно посмотреть несколько вариантов кастомизации, а также можно опробовать реакцию элемента прокрутки на изменение размеров окна браузера.
GitHub — репозиторий, где можно найти всё, что описывалось в статье
Default style — пример произвольной стилизации
Safari style — стилизация скроллбаров в стиле Mac OS 10.8

Заключение.

Стоит ли изобретать что-то новое? Безусловно! У каждой новой идеи есть шанс повлиять на будущее (в конкретном случае — будущее юзабилити)