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

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

Под катом можно узнать, как в ближайшее время будет работать скролл в 2ГИС Онлайн.

Механизм системного скролла реализуется на уровне базовых возможностей операционной системы, поэтому с уверенностью можно сказать, что он всегда лучше js-эмуляции: он производительнее, работает независимо от JavaScript и реализует все необходимые «фичи» системы для разного типа устройств.

Дизайн же системного скроллбара, особенно Windows младше 8 версии, способен изуродовать значительную часть сайтов интернета. То, что не все согласны с этим мириться, подтверждается фактом наличия большого числа решений, программно меняющих системный скроллбар на кастомный .

Сейчас в (и, соответственно, в ) мы используем FleXcroll: он эмулирует механизм скролла и не подходит нам по ряду причин:

  • Не является кроссплатформенным решением (в частности, плохо работает на Mac)
  • Как и любой эмулятор в принципе, имеет проблемы с производительностью
  • Не имеет встроенного механизма фиксации заголовков.
Все эти факторы заставили нас задуматься над двумя вопросами:
  • существует ли нужное нам готовое решение, или необходимо создавать собственное?
  • возможно ли в принципе сохранить системный механизм скролла, но полностью заменить его дизайн?
Нами были сформированы основные требования к решению, которое должно менять визуальное представление скроллбара:
  1. Системный механизм скролла должен быть сохранён: коррекции подвергается только его дизайн.
  2. Общий размер всех зависимостей должен быть минимизирован. В идеальном случае их не должно быть вообще. Должен быть минимизирован размер и самого решения.
  3. Должен присутствовать механизм фиксации заголовков контента при выходе их за поле зрения, либо простой интерфейс для добавления такого механизма (подробнее об этом пункте см. ниже).

Ограничения

На момент написания статьи, более или менее гибко кастомизировать скроллбар средствами CSS можно только в браузерах на движке webkit. Цвета скроллбара можно поменять в браузере Internet Explorer. В остальных браузерах поддержка кастомизации скроллбара через CSS полностью отсутствует. Отчасти это связано с жёсткой позицией w3c :
There are just some things that CSS should not do, full stop.

Существующие решения


Из того большого числа js-библиотек более половины подменяют нативный механизм скролла. Это значит, что для враппера свойство overflow выставляется в значение hidden, а вложенный контейнер с нужным нам контентом меняет свою абсолютную позицию при генерации событий, связанных со скроллом (например, mousewheel). К таким решениям можно отнести: jScrollPane , Scrollbar Paper , jQuery Custom Scrollbar plugin , FleXcroll , Tiny Scrollbar и многие другие.

При таком подходе возникает сразу два фундаментальных недостатка: отсутствие кроссбраузерности и отсутствие же кроссплатформенности. Дело в том, что интерфейс событий, генерируемых действиями пользователя, при помощи которых пользователь что-то скроллит, существенно отличается от браузера к браузеру: с точки зрения стандартов тут творится настоящий бардак. Более того, последовательность и логика «бросания» событий серьёзно отличается и между платформами. Например, трекпады на платформе Mac при скролле генерируют события типа Wheel с большей частотой, чем колесо обычной мыши, что приводит к чересчур быстрому скроллу в ряде подобных решений.

Именно эти недостатки эмуляции скролла привели нас к формулированию первого пункта требований.

Многие решения изначально позиционируются как плагины к jQuery. В ситуации, когда мы используем jQuery по частям, возникает проблема экономии трафика. Проблема существенно растёт при наличии у плагина зависимости от куда более тяжеловесного jQuery UI. Это касается, например, ShortScroll и Vertical Scroll. А также от ряда других библиотек: например, один из немногих jQuery плагинов, сохраняющих нативный механизм скролла, Scrollbars, зависит от 4 плагинов: event.drag, resize, mousehold и mousewheel общим весом более 10 кб.

Третьему пункту требований не удовлетворяет ни одно из найденных нами решений.

Собственное решение

У решения есть две основных задачи: 1) скрыть системный скроллбар и 2) отобразить кастомный скроллбар.

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

Для начала, построим html структуру:


где container - собственно, то, что мы хотим скроллировать; scroller - блок, в который по высоте не помещается container, но у него выставлено свойство overflow-y: scroll, что приводит к появлению системного скроллбара у его правой границы; wrapper - окно с шириной чуть меньшей, чем у scroller и свойством overflow: hidden. Ширина меньше ровно на ширину скроллбара scroller.

К сожалению, средствами CSS невозможно точно узнать ширину системного скроллбара. Например, не работает вариант с выставлением ширины 125% для scroller и 80% для container, при котором, казалось бы, ширины container и wrapper должны точно совпасть. Можно сделать scroller заведомо шире, а wrapper и container выставить одинаковую ширину, но такой способ не подходит для резиновой вёрстки и порождает баг в webkit браузерах (см. ниже).

Введём js-переменные:

Var wrapper = document.getElementById("wrapper"), scroller = document.getElementById("scroller"), container = document.getElementById("container");
Теперь мы можем вычислить ширину системного скроллбара: scroller.offsetWidth - это ширина scroller, включающая в себя border, padding, а также системный скроллбар. Если мы обнулим border и padding при помощи CSS, и вычтем scroller.clientWidth, мы получим искомую ширину скроллбара в пикселях.

В webkit-браузерах существует особенность, заставляющая скроллиться элементы при выделении в них текста в горизонтальном направлении, даже при overflow-x: hidden. То есть scroller начинает двигаться по горизонтали внутри меньшего по ширине wrapper, в результате чего обнажается скрытый нами системный скроллбар. К счастью, в webkit-браузерах, и только в них, мы можем обнулить ширину скроллбара средствами CSS, после чего ширины всех трёх блоков в точности совпадут и места для горизонтального скролла просто не будет:

Scroller::-webkit-scrollbar { width: 0; }
Теперь нарисуем и спозиционируем кастомный скроллбар. Для этого минимально усложним html структуру на 1 элемент, который и будет полностью отвечать за визуальное представление скроллбара:


Здесь важно отметить, что для нашей задачи не требовалась прорисовка кнопок «вверх» и «вниз», а также подстилающего «трека». Впрочем, никаких ограничений для их реализации нет.

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

Function dontStartSelect() { return false; } function selection(on) { if (on) { $(document).on("selectstart", dontStartSelect); } else { $(document).off("selectstart", dontStartSelect); } } event(bar, "mousedown", function(e) { e.preventDefault(); selection(false); // Disable text selection in IE8 }); event(document, "mouseup blur", function() { selection(true); // Enable text selection in IE8 });
Как видите, большая часть кода нужна для браузера IE8, который пока, к сожалению, нельзя сбрасывать со счетов. Обратите внимание, что сброс «нажатого» состояния мыши должен происходить не только при отпускании кнопки мыши, но и при потере страницей фокуса (blur).

Прилипающие заголовки

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

Ни абсолютное (относительно scroller), ни относительное (относительно своих начальных позиций) позиционирование заголовков в чистом виде, в данном случае не подходит. Первое - по причине схлапывания контента, окружающего заголовок и соответствующих им рывков при скролле; второе - по причине нативных неустранимых transition"ов у браузера Internet Explorer для скролла, которые приводят к дрожанию всех зафиксированных заголовков.

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

При каждом скролле для каждого заголовка необходимо проверять условие:

Scroller.scrollTop - H[i].offsetTop > Sum(H[j].offsetHeight, j=0..i-1) scroller.scrollTop - H[i].offsetTop < scroller.clientHeight - Sum(H[j].offsetHeight, j=i..n-1)
где H - массив элементов заголовков; i - номер заголовка, меняется в диапазоне от 0 до n-1; scroller.scrollTop - виртуальное расстояние от верхней границы container до верхней границы видимой части container; H[i].offsetTop - расстояние от верхней границы container до верхней границы заголовка H[i].

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

Пробрасывание события mousewheel

В webkit браузерах мы столкнулись с неприятным багом: событие mousewheel не пробрасывалось с фиксированных заголовков вверх к scroller. Это создавало эффект поломки (внезапного прекращения) скролла при попадании под курсор фиксированного заголовка (например, в результате всё того же скролла). То есть, пользователь крутит колесо мыши, под курсор попадает заголовок, фиксируется, и, внезапно, скролл перестаёт работать (точнее, скроллиться начинает вся страница).

К счастью, в webkit браузерах (а нам требовалось обратиться только к ним) есть такая возможность:

$(headers).on("mousewheel", function(e) { var evt = document.createEvent("WheelEvent"); evt.initWebKitWheelEvent(e.originalEvent.wheelDeltaX, e.originalEvent.wheelDeltaY); scroller.dispatchEvent(evt); // Пробрасываем событие на scroller e.preventDefault(); // Останавливаем скролл страницы });
Конечно, это сокращённая версия кода: необходимо проверять наличие соответствующих функций и типа события, чтоб не генерировать ошибки в других браузерах.

Пример

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

Например, в самом простом случае (без фиксации заголовков), инициализация выглядит так:

Baron($(".wrapper"), { scroller: ".scroller", container: ".container", bar: ".scroller__bar" });
Причём $(".wrapper") может быть массивом html объектов - проинициализируется каждый из них.

Если вы хотите фиксации заголовков, ограничения верхнего положения скроллбара, и использования альтернатив jQuery, инициализация немного усложняется:

Baron($(".test_advanced"), { scroller: ".scroller", container: ".container", bar: ".scroller__bar", barOnCls: ".scroller__bar_state_on", // Класс, навешиваемый скроллбару, когда он должен быть видимым barTop: 40, // Ограничитель позиции скроллбара сверху header: ".header__title", hFixCls: "header__title_state_fixed", // Класс, навешиваемый зафиксированным заголовкам selector: qwery, // Селектор event: function(elem, event, func, off) { // Менеджер событий if (off) { bean.off(elem, event, func); } else { bean.on(elem, event, func); } }, dom: bonzo // Библиотека для работы с DOM });
Для менеджера событий пришлось сделать обёртку, поскольку его интерфейс отличается в разных библиотеках. В принципе, можно обернуть нативные функции типа addEventListener и отказаться от специализированного менеджера событий.

Тестирование

В тестировании принимали участие актуальные версии браузеров Chrome, Firefox, Opera, Safari и Internet Explorer (IE) на платформах Windows, Mac и iOs (тестировались, конечно, только существующие версии браузеров:)). Кроме этого, тестировался IE9 и IE8. Все тесты на всех браузерах проходят нормально.

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

Итог

Предложенное решение позволяет сохранить механизм системного скролла, заменить его дизайн, полностью отказаться от жестких зависимостей от других библиотек, и уложиться в 998 байт сжатого кода (минифицированного и сжатого gzip). В довесок к этому, есть механизм фиксации каких-либо элементов скроллируемого контента (например, заголовков).

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

Код решения можно скачать с

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

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

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

1. NiceScroll.js

NiceScroll является jQuery плагином, который позволяет создать полосу прокрутки похожую на iOS.

2. NanoScroll

Данный плагин позволяет создать полосу прокрутки на вашем сайте как в Mac OS X.

3. jQuery custom content scroller

Плагин jQuery для стилизации полосы прокрутки в браузере с помощью CSS.

4. Tiny Scrollbar

Легкий jQuery плагин написанный с помощью jQuery библиотеки, который придаст красивый вид прокрутки контента и полосы прокрутки главного окна браузера.

5. Scrollbar Visibility

Легкий плагин написанный с помощью jQuery библиотеки, который придаст красивый вид прокрутки контента и полосы прокрутки главного окна браузера.

6. jScrollPane

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

7. Scrollbar Paper

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

8. jQuery Scrollbars v2

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

9. Vertical scrollbar

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

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

Существует несколько способов реализации пользовательской полосы прокрутки. В этом уроке мы будем использовать CSS3, что является самым простым способом. Конечно, есть jQuery-плагины, которые могут помочь с настройкой полосы прокрутки, но я не люблю добавлять много JavaScript на свой сайт. Если вы дизайнер, фотограф или просто хотите, чтобы на вашем сайте был крутой скроллбал, воспользуйтесь каким-нибудь jQuery-плагином.

Вы должны знать, что пользовательские полосы прокрутки настраиваются c помощью CSS свойств с префиксом -webkit в браузерах, использующих механизм рендеринга Webkit (и Blink).

Прежде чем начать, давайте рассмотрим из чего состоит скроллбар:

Терминология

Скролбар состоит из семи различных элементов:

::-webkit-scrollbar { /* 1 - скроллбар */ } ::-webkit-scrollbar-button { /* 2 - кнопка */ } ::-webkit-scrollbar-track { /* 3 - трек */ } ::-webkit-scrollbar-track-piece { /* 4 - видимая часть трека */ } ::-webkit-scrollbar-thumb { /* 5 - ползунок */ } ::-webkit-scrollbar-corner { /* 6 - уголок */ } ::-webkit-resizer { /* 7 - изменение размеров окна*/ }

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

Настройка

Создайте файлы index.html и style.css . В index.html поместите следующее:

Стилизация скроллбара с помощью CSS

Теперь займемся style.css

Во-первых, мы устанавливаем для класса .scrollbar width , height , background-color , затем устанавливаем overflow-y: scroll чтобы получить вертикальную полосу прокрутки. Мы задали min-height: 450px элементу с классом .force-overflow , чтобы появилась полоса прокрутки (т.к. мы использовали свойство overflow-y для скролла в классе .scrollbar ).

Scrollbar { background-color: #F5F5F5; float: left; height: 300px; margin-bottom: 25px; margin-left: 22px; margin-top: 40px; width: 65px; overflow-y: scroll; } .force-overflow { min-height: 450px; }

Теперь мы используем ::-webkit-scrollbar для стилизации полосы прокрутки. Он заменит ширину по умолчанию новой шириной, равной 6px и фоном с цветом #F5F5F5:

#style-1::-webkit-scrollbar { width: 6px; background-color: #F5F5F5; }

Теперь мы изменим вид ползунка на более привлекательный. Мы используем псевдоэлемент (т. е. ::- webkit-scrollbar-thumb ) и устанавливаем цвет ползунка - background-color .

#style-1::-webkit-scrollbar-thumb { background-color: #000000; }

После этих манипуляций ползунок выглядит так:


Вот и все, надеюсь, вам понравится!

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

Под катом можно узнать, как в ближайшее время будет работать скролл в 2ГИС Онлайн.

Механизм системного скролла реализуется на уровне базовых возможностей операционной системы, поэтому с уверенностью можно сказать, что он всегда лучше js-эмуляции: он производительнее, работает независимо от JavaScript и реализует все необходимые «фичи» системы для разного типа устройств.

Дизайн же системного скроллбара, особенно Windows младше 8 версии, способен изуродовать значительную часть сайтов интернета. То, что не все согласны с этим мириться, подтверждается фактом наличия большого числа решений, программно меняющих системный скроллбар на кастомный .

Сейчас в (и, соответственно, в ) мы используем FleXcroll: он эмулирует механизм скролла и не подходит нам по ряду причин:

  • Не является кроссплатформенным решением (в частности, плохо работает на Mac)
  • Как и любой эмулятор в принципе, имеет проблемы с производительностью
  • Не имеет встроенного механизма фиксации заголовков.
Все эти факторы заставили нас задуматься над двумя вопросами:
  • существует ли нужное нам готовое решение, или необходимо создавать собственное?
  • возможно ли в принципе сохранить системный механизм скролла, но полностью заменить его дизайн?
Нами были сформированы основные требования к решению, которое должно менять визуальное представление скроллбара:
  1. Системный механизм скролла должен быть сохранён: коррекции подвергается только его дизайн.
  2. Общий размер всех зависимостей должен быть минимизирован. В идеальном случае их не должно быть вообще. Должен быть минимизирован размер и самого решения.
  3. Должен присутствовать механизм фиксации заголовков контента при выходе их за поле зрения, либо простой интерфейс для добавления такого механизма (подробнее об этом пункте см. ниже).

Ограничения

На момент написания статьи, более или менее гибко кастомизировать скроллбар средствами CSS можно только в браузерах на движке webkit. Цвета скроллбара можно поменять в браузере Internet Explorer. В остальных браузерах поддержка кастомизации скроллбара через CSS полностью отсутствует. Отчасти это связано с жёсткой позицией w3c :
There are just some things that CSS should not do, full stop.

Существующие решения


Из того большого числа js-библиотек более половины подменяют нативный механизм скролла. Это значит, что для враппера свойство overflow выставляется в значение hidden, а вложенный контейнер с нужным нам контентом меняет свою абсолютную позицию при генерации событий, связанных со скроллом (например, mousewheel). К таким решениям можно отнести: jScrollPane , Scrollbar Paper , jQuery Custom Scrollbar plugin , FleXcroll , Tiny Scrollbar и многие другие.

При таком подходе возникает сразу два фундаментальных недостатка: отсутствие кроссбраузерности и отсутствие же кроссплатформенности. Дело в том, что интерфейс событий, генерируемых действиями пользователя, при помощи которых пользователь что-то скроллит, существенно отличается от браузера к браузеру: с точки зрения стандартов тут творится настоящий бардак. Более того, последовательность и логика «бросания» событий серьёзно отличается и между платформами. Например, трекпады на платформе Mac при скролле генерируют события типа Wheel с большей частотой, чем колесо обычной мыши, что приводит к чересчур быстрому скроллу в ряде подобных решений.

Именно эти недостатки эмуляции скролла привели нас к формулированию первого пункта требований.

Многие решения изначально позиционируются как плагины к jQuery. В ситуации, когда мы используем jQuery по частям, возникает проблема экономии трафика. Проблема существенно растёт при наличии у плагина зависимости от куда более тяжеловесного jQuery UI. Это касается, например, ShortScroll и Vertical Scroll. А также от ряда других библиотек: например, один из немногих jQuery плагинов, сохраняющих нативный механизм скролла, Scrollbars, зависит от 4 плагинов: event.drag, resize, mousehold и mousewheel общим весом более 10 кб.

Третьему пункту требований не удовлетворяет ни одно из найденных нами решений.

Собственное решение

У решения есть две основных задачи: 1) скрыть системный скроллбар и 2) отобразить кастомный скроллбар.

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

Для начала, построим html структуру:


где container - собственно, то, что мы хотим скроллировать; scroller - блок, в который по высоте не помещается container, но у него выставлено свойство overflow-y: scroll, что приводит к появлению системного скроллбара у его правой границы; wrapper - окно с шириной чуть меньшей, чем у scroller и свойством overflow: hidden. Ширина меньше ровно на ширину скроллбара scroller.

К сожалению, средствами CSS невозможно точно узнать ширину системного скроллбара. Например, не работает вариант с выставлением ширины 125% для scroller и 80% для container, при котором, казалось бы, ширины container и wrapper должны точно совпасть. Можно сделать scroller заведомо шире, а wrapper и container выставить одинаковую ширину, но такой способ не подходит для резиновой вёрстки и порождает баг в webkit браузерах (см. ниже).

Введём js-переменные:

Var wrapper = document.getElementById("wrapper"), scroller = document.getElementById("scroller"), container = document.getElementById("container");
Теперь мы можем вычислить ширину системного скроллбара: scroller.offsetWidth - это ширина scroller, включающая в себя border, padding, а также системный скроллбар. Если мы обнулим border и padding при помощи CSS, и вычтем scroller.clientWidth, мы получим искомую ширину скроллбара в пикселях.

В webkit-браузерах существует особенность, заставляющая скроллиться элементы при выделении в них текста в горизонтальном направлении, даже при overflow-x: hidden. То есть scroller начинает двигаться по горизонтали внутри меньшего по ширине wrapper, в результате чего обнажается скрытый нами системный скроллбар. К счастью, в webkit-браузерах, и только в них, мы можем обнулить ширину скроллбара средствами CSS, после чего ширины всех трёх блоков в точности совпадут и места для горизонтального скролла просто не будет:

Scroller::-webkit-scrollbar { width: 0; }
Теперь нарисуем и спозиционируем кастомный скроллбар. Для этого минимально усложним html структуру на 1 элемент, который и будет полностью отвечать за визуальное представление скроллбара:


Здесь важно отметить, что для нашей задачи не требовалась прорисовка кнопок «вверх» и «вниз», а также подстилающего «трека». Впрочем, никаких ограничений для их реализации нет.

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

Function dontStartSelect() { return false; } function selection(on) { if (on) { $(document).on("selectstart", dontStartSelect); } else { $(document).off("selectstart", dontStartSelect); } } event(bar, "mousedown", function(e) { e.preventDefault(); selection(false); // Disable text selection in IE8 }); event(document, "mouseup blur", function() { selection(true); // Enable text selection in IE8 });
Как видите, большая часть кода нужна для браузера IE8, который пока, к сожалению, нельзя сбрасывать со счетов. Обратите внимание, что сброс «нажатого» состояния мыши должен происходить не только при отпускании кнопки мыши, но и при потере страницей фокуса (blur).

Прилипающие заголовки

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

Ни абсолютное (относительно scroller), ни относительное (относительно своих начальных позиций) позиционирование заголовков в чистом виде, в данном случае не подходит. Первое - по причине схлапывания контента, окружающего заголовок и соответствующих им рывков при скролле; второе - по причине нативных неустранимых transition"ов у браузера Internet Explorer для скролла, которые приводят к дрожанию всех зафиксированных заголовков.

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

При каждом скролле для каждого заголовка необходимо проверять условие:

Scroller.scrollTop - H[i].offsetTop > Sum(H[j].offsetHeight, j=0..i-1) scroller.scrollTop - H[i].offsetTop < scroller.clientHeight - Sum(H[j].offsetHeight, j=i..n-1)
где H - массив элементов заголовков; i - номер заголовка, меняется в диапазоне от 0 до n-1; scroller.scrollTop - виртуальное расстояние от верхней границы container до верхней границы видимой части container; H[i].offsetTop - расстояние от верхней границы container до верхней границы заголовка H[i].

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

Пробрасывание события mousewheel

В webkit браузерах мы столкнулись с неприятным багом: событие mousewheel не пробрасывалось с фиксированных заголовков вверх к scroller. Это создавало эффект поломки (внезапного прекращения) скролла при попадании под курсор фиксированного заголовка (например, в результате всё того же скролла). То есть, пользователь крутит колесо мыши, под курсор попадает заголовок, фиксируется, и, внезапно, скролл перестаёт работать (точнее, скроллиться начинает вся страница).

К счастью, в webkit браузерах (а нам требовалось обратиться только к ним) есть такая возможность:

$(headers).on("mousewheel", function(e) { var evt = document.createEvent("WheelEvent"); evt.initWebKitWheelEvent(e.originalEvent.wheelDeltaX, e.originalEvent.wheelDeltaY); scroller.dispatchEvent(evt); // Пробрасываем событие на scroller e.preventDefault(); // Останавливаем скролл страницы });
Конечно, это сокращённая версия кода: необходимо проверять наличие соответствующих функций и типа события, чтоб не генерировать ошибки в других браузерах.

Пример

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

Например, в самом простом случае (без фиксации заголовков), инициализация выглядит так:

Baron($(".wrapper"), { scroller: ".scroller", container: ".container", bar: ".scroller__bar" });
Причём $(".wrapper") может быть массивом html объектов - проинициализируется каждый из них.

Если вы хотите фиксации заголовков, ограничения верхнего положения скроллбара, и использования альтернатив jQuery, инициализация немного усложняется:

Baron($(".test_advanced"), { scroller: ".scroller", container: ".container", bar: ".scroller__bar", barOnCls: ".scroller__bar_state_on", // Класс, навешиваемый скроллбару, когда он должен быть видимым barTop: 40, // Ограничитель позиции скроллбара сверху header: ".header__title", hFixCls: "header__title_state_fixed", // Класс, навешиваемый зафиксированным заголовкам selector: qwery, // Селектор event: function(elem, event, func, off) { // Менеджер событий if (off) { bean.off(elem, event, func); } else { bean.on(elem, event, func); } }, dom: bonzo // Библиотека для работы с DOM });
Для менеджера событий пришлось сделать обёртку, поскольку его интерфейс отличается в разных библиотеках. В принципе, можно обернуть нативные функции типа addEventListener и отказаться от специализированного менеджера событий.

Тестирование

В тестировании принимали участие актуальные версии браузеров Chrome, Firefox, Opera, Safari и Internet Explorer (IE) на платформах Windows, Mac и iOs (тестировались, конечно, только существующие версии браузеров:)). Кроме этого, тестировался IE9 и IE8. Все тесты на всех браузерах проходят нормально.

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

Итог

Предложенное решение позволяет сохранить механизм системного скролла, заменить его дизайн, полностью отказаться от жестких зависимостей от других библиотек, и уложиться в 998 байт сжатого кода (минифицированного и сжатого gzip). В довесок к этому, есть механизм фиксации каких-либо элементов скроллируемого контента (например, заголовков).

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

Код решения можно скачать с

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

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

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

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

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

content...

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

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

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

Итак.

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

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

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

  • корневой блок является оберткой для всей конструкции и определяет область и размеры всей конструкции в целом;
  • 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.clientHeight - viewport.height(); // корректировка по высоте var correct_w = systemscrolls.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, вызывая функцию перемещения бегунков.

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

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

Вот и дошло время до весьма важной функции - обновление параметров компонента. Для этой функции необходимо обеспечить скорость выполнения, т.к. она может вызываться при ресайзе и смене контента очень часто, поэтому большая часть её написана на чистом 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 мс, нагрузки на браузер почти никакой и все весьма надежно.

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

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

Эта верстка вертикального скроллбара (для горизонтального всё подобно). Преимущество такой структуры следующие:
  • обобщающие классы 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()



Top