Полный контроль над контрольными точками. В CSS и в JS
Всем привет! Недавно меня поругали за то, что для гарантии адаптивности вёрстки я применяю дополнительный класс .js-adaptive
, который я вешаю на элемент body
с помощью JavaScript. К примеру, при ширине экрана 600px
и ниже класс .js-adaptive
вешается на body
, а при ширине 601px
и выше этот класс с body
убирается. В самих стилях при ширине экрана 600px
и ниже я делаю адаптивность вот так:
.block-container { display: block // при ширире больше 600px включаем display: block для .block-container } .js-adaptive .block-container { display: flex // при ширине меньше 600px включаем display: flex для .block-container }
Здесь вырисовывается явный минус. В адаптивном режиме у нас появляется дополнительный класс .js-adaptive
перед всеми селекторами, из-за чего повышается специфичность. Любителям БЭМа такое вряд ли понравится, да и вообще, не очень-то это и хороший подход на самом деле, тем более, что для таких целей у нас есть медиавыражения в CSS.
Сразу уточню, что решение данной проблемы у меня нашлось, и даже не одно. Для самых нетерпеливых сразу же дам ссылку на них.
Почему я обратился к дополнительному классу .js-adaptive
? Дело в том, что на практике оказалось, что медиавыражения в CSS расходятся во мнениях с такими штуками, как $(window).width()
и window.innerWidth
. Чуть позже я на примерах покажу, что я имею ввиду, а пока я поясню, зачем мне понадобились $(window).width()
и window.innerWidth
вместе с медиавыражениями.
Задача
Представьте себе такую задачу. Есть навигация, в которую мы для маленькой ширины (<=600px) из двух разных меню делаем одно гамбургерное (объединяя их пункты), а при ширине 601px
и выше эти меню должны снова делиться на два. Понятно, что для этого мы используем JavaScript, чтобы отловить ширину и действовать в зависимости от неё. А теперь представьте, что вдобавок к этому при ширине <=600px
должна включаться адаптивность, то есть все блоки/элементы должны менять свой внешний вид, готовясь к мобильным экранам. Согласитесь, что задача такого совмещения JavaScript (для своих целей) и медиавыражений (так же для своих целей) очень частая? Собственно, это и есть та причина, по которой нам нужно использовать JavaScript вместе с медиавыражениями.
Проблема
И вот тут как раз возникает проблема. Для наглядности я сделал примеры на codepen.io. Поскольку наша задача состоит в том, чтобы использовать в одну единицу времени сразу $(window).width()
(или window.innerWidth) вместе с медиавыражениями, то очевидно, что контрольная точка (в нашем случае 600px
) у тех и у других должны совпадать. И проблема в том, что они не совпадают.
Покажу на примерах
1) Пример без скролла — в этом примере верхний div
отвечает за медиавыражения, нижний за $(window).width()
(или window.innerWidth
), ну а span
— это просто линейка в 600px
, правый край которой и является нашей контрольной точкой (600px
).
Здесь всё работает отлично. Сузьте экран ровно до правого края span
и увидите, как все два div
станут синими. А если жмякнуть кнопкой мыши на span
, то даже в консоли всё будет ровненько по 600px
.
2) Но всё здорово ровно до того момента, пока не появляется вертикальный скролл. Сравним вариант со скроллом. Теперь во всех браузерах кроме Safari сужение экрана до 600px
врубает $(window).width()
, а вот медиавыражение срабатывает только на 585px
. Понятно, что медиавыражение учитывает ширину скролла (у меня это 15px
). Но разночтение с $(window).width()
уже очень расстраивает. А вот в Safari всё срабатывает так же, как без скролла, то есть ровненько на 600px
. Следовательно, помимо разночтения при скролле мы ещё имеем разночтения в браузерах (Safari vs остальных).
3) Ок, попробуем поменять $(window).width()
на window.innerWidth
— Вариант со скроллом и window.innerWidth
. Теперь во всех браузерах (в Edge не тестил, не суть) кроме Safari медиавыражения и window.innerWidth срабатывают в одной точке — 585px
, а вот в Safari медиавыражения срабатывают на 600px
, а вот window.innerWidth
на 585px
.
Промежуточный итог:
Получается, что в данном случае нет единого мнения среди браузеров, и у нас нет возможности контролировать медиавыражения вместе с $(window).width()
(или window.innerWidth
)… или всё же есть? А давайте это и проверим в следующем разделе…
Решение проблемы:
Первым делом, для решения задачи, я как и положено верстальщику, отправился в гугл, и как оказалось, я не первый, кто задавался подобным вопросом, а толкового решения в итоге так и не нашлось. И даже с помощью статьи по теме на https://learn.javascript.ru я ничего не смог сделать. Поэтому мне пришлось кинуть клич во все инстанции чаты/слаки/телеграммы и т.д, о чём я ни капли не пожалел, поскольку добрые и более опытные коллеги поделились со мной аж пятью решениями! Поэтому давайте уже скорее рассмотрим каждое из них:)
1. Хак со свойством font-family
элемента <html>
Первое решение подсказал Станислав Ивашкевич (@_sssats). Его идея заключается в том, чтобы вместо измерения скриптом ширины окна проверить, сработало ли медиавыражение, по его результату. И для этого в медиавыражении мы присваиваем свойству font-family
элемента <html>
значение mobile
(можно присвоить любое на ваш вкус, главное, чтобы оно было без кавычек, иначе в в Safari, например, это не сработает), а уже в JavaScript при ресайзе окна просто ждём момента, когда медиавыражение сработает, и всё. Это гарантирует, что адаптивный режим при <=600px
включится в одной контрольной точке, и в медиавыражении и в JavaScript.
Да, само собой, можно сделать сколько угодно медиавыражений для разных устройств и экранов. Вот так, к примеру, Станислав использует этот трюк в продакшне.
Развитие идеи
Мы с моим коллегой SelenIT-ом решили немного развить эту идею. Дело в том, что менять что-либо у элемента <html>
немного рисковано, во-первых, потому, что от этого элемента всё наследуется, а во-вторых, может так получится, что у пользователя на компе случайно окажется шрифт mobile
, и тогда проблем не миновать. Ведь этот трюк хорош тем, что позволяет использовать не только элемент <html>
или свойство font-family
, а вообще что угодно. Поэтому мы решили оставить элемент <html>
, но вместо font-family
использовать псевдоэлемент ::before
и его значение content()
, в котором можно уже писать любой текст.
@media screen and (min-width: 1px) and (max-width:600px) { html::before { content: 'Маленький экран'; display: none; } ...
В общем, вот что у нас вышло. Поясню некоторые моменты в коде.
var MQ_indicator = window.getComputedStyle(document.querySelector('html'), ':before').getPropertyValue('content').replace(/["']/g,'');
Этой строкой мы вычисляем значение свойства content, а далее просто проверяем его при помощи условия…
if(MQ_indicator == 'Маленький экран') {
… и как только оно сработает, ловушка захлопнута :)
Решение с помощью кастомных свойств
Обновление от 21.05.2017: ещё один способ подсказал Сергей Артёмов (@firefoxic_arts). Он решил пойти ещё дальше и вместо всяких хаков задействовал кастомные свойства. А суть решения следующая:
Для начала мы объявляем значение переменной --media
в селекторе :root
:root { --media: mobile-up; }
Далее в медиавыражении для мобильных экранов переопределяем значение нашей переменной:
@media (max-width: 600px) { :root { --media: mobile-only; } ...
Ну а далее в JavaScript делаем вот что:
/* Сохраняем в переменной exportMediaToJs ссылку на селектор с классом '.export-to-js' (тестовый div, который отвечает за JavaScript */ const exportMediaToJs = document.querySelector('.export-to-js'); /* Определяем функцию, в которой будут происходить все проверки */ const repaint = function () { /* Получаем значение свойства --media для селектора :root, а после с помощью метода .trim() удаляем ненужные проблемы, которые мы ставим после двоеточия в --media: */ let mediaQuery = getComputedStyle(document.querySelector(':root')).getPropertyValue('--media').trim(); /* Задаем переменной exportMediaToJsColor значение по умолчанию "orange" */ let exportMediaToJsColor = 'orange'; /* Проверяем, если значением свойства --media является 'mobile-only', то это и есть граница нашей контрольной точки*/ if (mediaQuery === 'mobile-only') { exportMediaToJsColor = 'lightblue'; } /* Устанавливаем тестовому элементу div (отвечающему за JavaScript) цвет, который зависит от того, сработало ли медиавыражение или нет. */ exportMediaToJs.style.setProperty('--color', exportMediaToJsColor); }; /* Вешаем на события 'resize' и 'load' нашу функцию выше */ window.addEventListener('resize', repaint); window.addEventListener('load', repaint);
Вот собственно и всё. Нам с SelenIT-ом очень нравится решение Сергея, поскольку оно использует CSS-переменные, которые по сути и предназначены для таких целей, и ещё вдобавок с ними код становится чище и избавляет нас от ненужных хаков. Браво Сергей! Единственный минус (а минус ли это?) здесь в том, что кастомные свойства поддерживаются только в современных браузерах, включая Edge 15+. Поэтому смело используйте его, если вам не требуется поддержка IE10-11.
2. Хак с определением display
у блока-хелпера
Это решение принадлежит Виталию Емельянцеву (@gambala_rus). Оно немного напоминает предыдущий вариант, но вместо font-family
у <html>
здесь используется блок-хелпер:
<div class="device-helper_visible-below-600"></div>
Поначалу мы скрываем его в CSS с помощью display: none
, а после в медиавыражении меняем его display
на block
:
.device-helper_visible-below-600 { display: none; // скрываем элемент-хелпер } @media screen and (min-width: 1px) and (max-width:600px) { .device-helper_visible-below-600 { display: block; // показываем элемент-хелпер }
Далее уже в JavaScript мы создаём функцию, которая возвращает true/false
в зависимости от значения свойства display у элемента-хелпера:
var deviceHelper = $(".device-helper_visible-below-600"); // Получаем ссылку на элемент-хелпер var isBreakpoint = function() { return deviceHelper.is(':visible'); // возвращаем true/false в зависимости от значения display у элемента-хелпера };
Ну и последним действием мы просто проверяем результат, который вернула функция, и уже пляшем от него:
if (isBreakpoint()) {
Вот собственно и всё, ничего сложного, поэтому не будем тут задерживаться и перейдём к следующему решению.
Кстати, у Виталия есть свой чат в Telegram под названием «Школа Веб 2.0», где он с радостью помогает новичкам (да и не только), отвечая на разные вопросы. Смело задавайте ему вопросы по веб-разработке, CSS, HTML, JS, Ruby on Rails, Дизайну, UI/UX, тайму и таск-менеджменту.
3. Решение с помощью window.matchMedia()
Добрые люди подсказали мне, что оказывается есть такая штука, как window.matchMedia()
— метод, принимающий в качестве аргумента строку — наше медиавыражение ("screen and (min-width: 1px) and (max-width:600px)"
). Он возвращает объект MediaQueryList, у которого есть свойство matches, возвращающее true/false
в зависимости от того, совпал ли запрос с размерами экрана или нет. Выглядит это очень просто:
if (window.matchMedia("screen and (min-width: 1px) and (max-width:600px)").matches) { // свойство matches вернуло true, поскольку ширина экрана от 1 до 600px. }
Поскольку в объекте MediaQueryList мы проверяем то же самое медиавыражение, что в CSS, и по той же самой логике, мы можем быть уверены, что срабатывать оно будет в той же самой точке.
Насчёт поддержки браузами matchMedia можете вообще не париться, она прекрасна. IE10+, Safari и другие современные браузеры поддерживают эту штуку на ура!
Кстати, я настоятельно рекомендую познакомиться с этим window.matchMedia()
поближе, поскольку его возможности не ограничиваются нашей задачей. Советую начать вот с этой статьи на русском, а после обратиться к MDN (раз, два) и самой спецификации.
4. Хак с определением ширины скроллбара + window.matchMedia()
А это решение принадлежит Владимиру Кузнецову (@mista_k). Уверен, что вы уже читали его популярный блог. Его идея состоит из нескольких этапов. Давайте разберём их.
Первым этапом будет вычисление ширины скроллбара. Для этого мы создаём следующую функцию:
function getScrollbarWidth() { /* Создаём элемент div с position: absolute (и закидываем его в body). Прячем его за экран с помощью ствойств top и left. А уже внутрь этого div кладём ещё один div. var div будет служить ссылкой на внешний div. */ var div = $('<div style="width:50px; height:50px; overflow:hidden; position:absolute; top:-200px; left:-200px;"><div style="height:100px;"> </div></div>').appendTo('body'); /* Измеряем внутреннюю ширину внутреннего div, и кладём результат в переменную */ var w1 = $('div', div).innerWidth(); /* Принудительно врубаем у внешнего div скролл */ var w1 div.css('overflow-y', 'scroll'); /* Заново измеряем внутреннюю ширину div, но уже со скроллом (если, конечно, скролл есть), и кладём результат в переменную var w2 */ var w2 = $('div', div).innerWidth(); /* Удаляем div */ $(div).remove(); /* Возвращаем результат ширины скролла. Если его нет, то вернётся 0 */ return (w1 - w2); }
Далее нам нужно выяснить, учитывает ли браузер ширину скроллбара в медиавыражениях или нет, чтобы потом вычесть эту ширину из общих расчётов. К примеру, Sarari может не учитывать, а остальные браузеры — наоборот. Для этого мы создаём такую вот функцию:
function getDifference() { /* Получаем ширину странцы */ var windowWidth = $(window).width(); /* Получаем ширину скроллбара из предыдущей функции (если скроллбара нет, то вернётся 0) */ var scrollbarWidth = getScrollbarWidth(); /* Создаём медиавыражение с учётом полученных результатов ранее */ var query = 'screen and (min-width: 1px) and (max-width:' + (windowWidth + scrollbarWidth) + 'px)'; /* С помощью уже знакомого нам метода window.matchMedia и свойства matches проверяем, учитывает ли браузер ширину скроллбара при срабатывании медиавыражений или нет. Если да, то возвращает эту ширину. */ if (window.matchMedia(query).matches) { return scrollbarWidth; } /* Если браузер не учитывает ширину скроллбара, то возвращаем 0 */ return 0; }
Ну а дальше всё просто. Присваиваем результат предыдущих проверок переменной var widthDiff = getDifference();
, а после уже при ресайзе в условии отнимаем результат этой переменной от контрольной точки:
$(window).on("resize", function() { var windowsWidht1 = $(window).width(); /* изменяем контрольную точку, если знаем, что скролбар влияет на медиавыражения */ if (windowsWidht1 <= 600 - widthDiff) {
По идее это исключает любые риски, связанные с поведением браузера в отношении скроллбара при медиавыражениях. Сам способ немного замороченный, но зато он гарантирует, что медиавыражения и условия в JavaScript будут срабатывать одновременно. А чтобы совсем было понятно, как он работает, Владимир подробно описал это прямо в примере на codepen.
5. Решение с определением браузера Safari
Это решение родилось благодаря Инне Сукновальник (@isuknovalnik). Оказывается Инна уже сама давно задавалась этим вопросом, и накопала по нему много интересных деталей. Как она выяснила, во всех браузерах, кроме Safari медиавыражения срабатывают по window.innerWidth
, т.е. скроллбар включается в ширину окна. И так должно быть по спецификации. А вот в Safari как раз всё наоборот — он противоречит спецификации, и медиавыражения в нём срабатывают по document.documentElement.clientWidth
, а ширина окна не включает скроллбар. Поэтому Инна пошла следующим путём: поскольку плохо себя ведёт только лишь Safari, то с помощью скрипта мы можем определить этот браузер и подсовывать ему document.documentElement.clientWidth
, а остальным window.innerWidth
.
Сразу скажу, что я не стал сильно шерстить интернет в поисках подходящего скрипта для определения браузера, и взял почти первый попавшийся, поскольку мне важна была сама суть:
function getdocWidth(){ /* Получаем строку из юзерагента браузера */ var ua = navigator.userAgent.toLowerCase(); /* Проверяем, если в строке есть "safari", то скорее всего это webkit, поэтому заходим в этот if */ if (ua.indexOf('safari') != -1) { /* Если это браузер на основе Chrome, то записываем в переменную docWidth значение window.innerWidth */ if (ua.indexOf('chrome') > -1) { docWidth = window.innerWidth; /* Если это не Chrome, то значит это Safari, поэтому в переменной docWidth уже сохраняем значение document.documentElement.clientWidth*/ } else { docWidth = document.documentElement.clientWidth; } /* Если в строке юзерагента нет "Safari", значит это какой-то иной браузер, поэтому отдаём ему window.innerWidth */ }else{ docWidth = window.innerWidth; } /* Ну и возвращаем переменную */ return docWidth; }
А далее при ресайзе окна нам лишь нужно вернуть результат этой функции и дальше действовать по уже знакомому нам сценарию:
$(window).on('resize', function () { var docWidth = getdocWidth() if(docWidth <= 600) { ...
Вот такой вот нехитрый способ. Как по мне, то я бы не стал особо ему доверять, поскольку может случится, что в будущем Safari захочет исправить это поведение, начав действовать по спецификации, и тогда этот метод может подпортить нам настроение.
Заключение
Вам, наверное, не терпится задать вопрос, мол, зачем нужны 1-, 2-, 4-, 5-е решения, если для этого есть специально придуманный нативный метод matchMedia (3-е решение), который ещё и имеет отличную поддержку браузерами? Да, возможно вы правы, мне и самому, если честно, этот вариант импонирует больше всего. Но мы рассмотрели все эти способы как минимум для того, чтобы почерпнуть какие-то идеи, которые могут пригодиться в будущем и в других задачах, а так же чтобы узнать что-то новое. Поэтому, я очень надеюсь, что в комментариях вы сможете предложить и другие идеи, которые я с радостью включу в статью. Да и просто делитесь своими мыслями на этот счёт, тоже будет интересно послушать.
Кстати, Владимир (@mista_k) настоятельно рекомендовал перестраховываться, и совмещать метод с matchMedia
, к примеру, с хаком с ::before
для элемента <html>
(Решение с matchMedia и хаком с ::before
у элемента <html>
). Это исключит любые риски, связанные с задержкой загрузки стилей, когда JS уже загрузился. Подробнее об этом можете почитать в комментарии Владимира и в его статье по теме.
Все решения воедино:
- Хак со свойством
font-family
элемента<html>
(от @_sssats) - Хак с сохранением названия сработавшего MQ в невидимом псевдоэлементе элемента
<html>
- Решение с помощью кастомных свойств (от @firefoxic_arts)
- Хак с определением
display
у блока-хелпера (от @gambala_rus) - Решение с помощью
window.matchMedia()
- Хак с определением ширины скроллбара +
window.matchMedia()
(от @mista_k) - Решение с определением браузера Safari (от @isuknovalnik)
P.S. Это тоже может быть интересно:
По правде сказать, для решения исходной задачи нужно только
matchMedia()
.А вот проверка на то, применяются конкретные стили с учётом медиа-запроса или нет, которая описана в пунктах 1 и 2 может быть хорошей страховкой от случайных расхождений в логике.
Я аналогичный случай описывал в статье Динамическая загрузка «отзывчивых» ресурсов. Технически, может оказаться, что скрипты загрузились и
matchMedia()
срабатывает, а вот стилей, соответствующих этой логике на странице нет.Владимир, благодарю за коммент. Но есть вопрос. У вас хоть и надёжный, но сложноватый скрипт. Нельзя ли сделать попроще: к примеру, взять вариант с
matchMedia()
и просто внутри условия делать дополнительную проверку на тот жеz-index
у элемента:if (window.matchMedia("screen and (min-width: 1px) and (max-width:600px)").matches) {
if($('html').css('z-index') == '1') {
// то мы точно знаем, что медиавыражение сработало
Максим, конечно можно. Я исходил из кода, который вы запостили. А потом в комментарии написал, что нужно использовать только
matchMedia()
для синхронизации CSS и JS кода.Вот мой «чистый» пример: https://codepen.io/mistakster/pen/GmwBpQ
В зависимости от ширины экрана будет меняться вид меню: строка или селектор. А вся важная логика скрыта в последних тридцати строках кода. Там нет никаких проверок на ширину скролбара, нет обработчиков
window.on('resize')
и т.п.Дайте знать, если и после этого остались какие-то вопросы. Можно обсудить в скайпе, например.
Да нее, не стоит, спасибо, Владимир! Просто скажите, как вы думаете, тогда вот такого совмещённого решения ( matchMedia и хак с ::before у элемента html) хватит, чтобы исключить все риски?
Думаю, что хватит. :-)
Спасибо, добавил ваш ответ в «Заключение»!
Перестаньте писать костыли на JS, обходитесь стабильным css (медиа запросы) !
В статье подробно описывается, почему приходится прибегать к костылям.
А почему нельзя так?
https://codepen.io/anon/pen/KmbKOd?editors=0010
* задержка выставлена специально
ну вы сами то проверяли что у вас получилось?
А почему нельзя вычислять ширину скролла у окна браузера (window.innerWidth — document.body.clientWidth)? У Дивов он такой же будет.
Покажите итоговый вариант, пожалуйста. Например, чуть исправьте вот этот
Я в данном случае про функцию getScrollbarWidth() писал из промежуточного варината (4). Иногда ширину скролбара все-равно полезно знать (Например, когда есть фиксированная шапка + основная часть со скроллом)
https://codepen.io/anon/pen/KmJKKx
+ еще один хак
Извиняюсь, но у вас по ссылке я не увидел никакого варианта. У вас просто используется matchMedia(). Или вы забыли сохранить пример?
Видимо я сам до конца не разобрался в чем проблема. У меня в Safari блоки не перекрашиваются одновременно только при загрузке. При расайзе все происходит нормально. Если код вынести в отдельную функцию и ее вызывать при загрузке и при ресайзе, либо не изменяя код добавить $(window).trigger(‘resize’), то все работает.
Да, но просто выходит, что вы привели то же решение, что и у нас в статье :)
В ветке был приведен пример с matchMedia, и я подумал что проблема в Safari применима и к нему. Если говорить про первоначальный вариант, то можно можно также определять safari css hackoм через медиа выражения.
https://codepen.io/anon/pen/qmgeGK?editors=0010
В любом случае вариант с matchMedia самый правильный.
Максим, вы проблему из пальца высосали и раздули до целой статьи. Никаких расхождений нет. Почему, я вам ответил на форуме, продублирую и сюда тоже.
Ну во первых вы изначально сами себя запутали отождествляя window.innerWidth с $(window).width() который на самом деле является аналогом document.documentElement.offsetWidth . Эти методы ( window.innerWidth и document.documentElement.offsetWidth ) позволяют получить ширину вьюпорта со скролом и соответственно без. C Сафари проблем быть не должно, не под виндой случаем запускаем )?
И ещё для медиазапросов в JS есть свои методы
var smallScreen = window.matchMedia(«(max-width: 600px)»).matches;
if ( smallScreen ) {
}
Андрей, к сожалению не могу с вами согласиться, поскольку проблема всё же есть, и не только я это понял. Про window.matchMedia теперь знаю, но и оно тоже может подвести (см. «Заключение»). И если вам не сложно, покажите, пожалуйста, свой рабочий вариант не на window.matchMedia, а именно с document.documentElement.offsetWidth и иже с ним.
Максим, я прочитал заключение и не совсем понял при каких обстоятельствах может подвести matchMedia.
PS Ваша статья называется — «Полный контроль над контрольными точками. В CSS и в JS» а не «Как отказаться от matchMedia и усложнить себе жизнь», но если сильно хочется можно использовать
window.innerWidth. Насколько я понимаю это решение у вас работало везде кроме сафари, но вы не уточнили на какой платформе запускали последний. Сафари под windows давно мертв и поддерживать его не имеет смысла.
В том-то и дело, что Safari на Mac.
расхождения есть и это проблема, позвоните по скайпу мне, я вам покажу как это работает, скайп vejevich
Далеко ходить не надо, попробуйте покрутите эту ссылку на разных браузерах и поймете http://www.456bereastreet.com/lab/media-query-width/
Интересно не изменился ли результат из-за вот этих вот строк в Твиттере твит
Так и не понял зачем вообще js для этого? Всю адаптивность легко можно делать на чистом css
также проблема в использовании единиц измерения vh, vw. Есть разница в сафари и хром
Ребята, привет!
Спасибо за публикации.
Классная статья, хотел распечатать — да не получается хорошо распечатать, не видно кода, который прячется за горизонтальной полосой прокрутки (конечно, допишу ручками, благо — его не так много). :))
У меня есть для вас предложение: Давайте примеры кода в статьях не размещать больше, чем ширина самой статьи, чтобы не появлялись эти горизонтальные полосы прокрутки и можно было бы статью не читать с компа, а распечатывать. Ну, и горизонтальная полоса прокрутки — не лучшее удобство для комфортного чтения статей.
P.S. В принципе уже существует выверенное количество символов в одной строке, которые удобны для чтения. Упоминание об этом в статье тут: https://css-tricks.com/bookmarklet-colorize-text-45-75-characters-line-length-testing/