Изменение цвета шрифта с помощью CSS в зависимости от фона

Изменение цвета шрифта с помощью CSS в зависимости от фона

Перевод статьи Switch font color for different backgrounds with CSS

Вы когда-нибудь ловили себя на мысли «Я могу это сделать на одном CSS!», когда кто-то демонстрировал свои JavaScript-бицепсы? Именно это я чувствовал, когда смотрел Dag-Inge Aas & Ida Aalen talk at CSSconf EU 2018. Ребята из Норвегии, где WCAG-стандарты доступности не просто хорошая практика, а обязательное по закону условие. Они разрабатывали новую фичу для того, чтобы пользователь мог изменять цветовую тему сайта, и столкнулись с проблемой автоматической подстройки цвета шрифта на основе выбранного цвета фона. Если фон темный, было бы идеально иметь белый текст для контраста, но что если будет выбран светлый фон? Текст с ним просто сольется.

Они элегантно решили проблему с помощью npm-пакета «color», добавления условных рамок и автоматического расчета дополнительного цвета. Но они сделали это на JavaScript. А вот мое альтернативное CSS-решение.

Вызов

Я поставил перед собой такие задачи:

  • изменять color на белый или черный в зависимости от фона;
  • ту же логику применять для рамок, используя более темный вариант цвета фона кнопки для улучшения ее видимости, только если фон очень светлый;
  • автоматически создавать дополнительный цвет, изменяя параметр hue на 60° по цветовому кругу.

Работа с HSL-форматом и CSS-переменными

Самый простой подход, который я cмог придумать для этого, подразумевает использование HSL-формата цвета. При этом каждый параметр является CSS-переменной, что позволяет очень просто определять светлоту (lightness) и использовать ее в условиях.

Теперь мы сможем изменять цвет фона прямо в рантайме, используя CSS-переменные и условия if/else.

Подождите… но ведь в CSS нет условий if/else… или есть?

Введение в условные выражения CSS

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

Этот трюк основан на том факте, что некоторые свойства в CSS имеют минимальное и максимальное значение. Например, прозрачность. Валидные значения находятся в диапазоне 0-1. Но если вы объявим opacity: 2, или 3, или даже 1000 это будет интерпретироваться как 1. А отрицательные значения превратятся в 0.

Применение фокуса к цвету текста

Параметр lightness в HSL-декларации ведет себя точно так же, превращая все отрицательные значения в 0 (черный цвет, независимо от значений hue и saturation), а все значения больше 100%, соответственно, в 100% (всегда белый).

Таким образом, можно объявить цвет, вычесть сколько нужно из параметра светлоты и умножить на 100%, чтобы получить итоговое значение: либо меньше ноля, либо больше 100%. Поскольку для белого цвета нужны отрицательные значения, а для черного положительные, полученный результат нужно инвертировать, умножив на -1.

Давайте пройдемся по этому коду.

  • Мы начинаем со значения светлоты 80 и устанавливаем порог на 60.
  • Вычитаем одно из другого и получаем 20.
  • Умножаем на -100%. Итоговый результат равен -2000%, что преобразуется в 0%.

Наш фон светлее порогового значения, поэтому мы считаем его «светлым» и используем черный цвет для текста.

Если бы переменная --light имела значение 20, то в результате вычислений мы получили бы 4000%, то есть 100%. Для темного фона — белый текст.

Создание рамки по условию

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

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

Если оттенок равен 0, а насыщенность 100%, для фона со светлотой более 80% рамка будет непрозрачной красной, а для более темного фона — полностью прозрачной (как будто ее нет).

Установка дополнительного цвета со смещением на 60°

Это, вероятно, самое простое. Можно использовать один из двух способов:

  1. filter: hue-rotate(60). Это первое решение, которое приходит в голову, но оно не самое лучшее, потому что может повлиять на дочерние элементы. Если необходимо, его эффект можно отменить противоположным фильтром.
  2. HSL hue + 60. Просто добавить 60 к hue. Значение не лимитировано числом 360, оно спокойно прокручивается по цветовому кругу. Таким образом 400deg=40deg, 480deg=120deg и так далее.

Учитывая это, мы можем добавить класс-модификатор для наших дополнительных элементов, который добавляет 60 к значению оттенка. Поскольку переменные в CSS не могу самоизменяться (т. е. нет такой вещи, как --hue: calc (var (--hue) + 60)), нужно добавить новую вспомогательную переменную для манипуляции с оттенком.

А затем переопределить ее в классе-модификаторе:

Самое лучшее в этом подходе — это автоматический пересчет и применение всех значений.

Собрав все вместе, мы получаем решение для всех трех поставленных задач на чистом CSS.

See the Pen CSS Automatic switch font color depending on element background…. FAIL by Facundo Corradini (@facundocorradini) on CodePen.0

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

Что, во имя CSS, с этим делать?

Восприятие яркости

Яркость в нашем восприятии не соответствует HSL-светлоте. Но, к счастью, мы можем ее измерить и адаптировать код должным образом, чтобы он также учитывал оттенок цвета.

Для этого присвоим каждому из трех основных цветов коэффициент восприятия человеческим глазом. Обычно это называется luma.

Существует несколько методов, вот два самых популярных:

  • sRGB Luma (ITU Rec. 709):
    L = (red * 0.2126 + green * 0.7152 + blue * 0.0722) / 255
  • W3C-метод (working draft):
    L = (red * 0.299 + green * 0.587 + blue * 0.114) / 255

Скорректированные расчеты

Первое очевидное последствие использование подхода с коррекцией яркости заключается в том, что мы не можем использовать формат HSL, так как CSS не может конвертировать его в RGB самостоятельно.

Поэтому придется перейти на RGB-формат для фона, вычислить параметр luma с помощью любого метода и использовать его в объявлении цвета (которое останется в HSL-формате).

Для рамок нужно использовать RGBA и с помощью альфа-канала установить прозрачность. Более темный оттенок получается вычитанием 50 из каждого канала.

В WebKit на iOS есть странная ошибка: если в кратком объявлении свойства border в формате RGBA использовать переменные, рамка отображается черной. Чтобы этого избежать, нужно использовать развернутое объявление.

Так как мы больше не используем HSL для фона, то для получение дополнительного цвета воспользуемся фильтром:

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

И вот окончательный вариант:

See the Pen CSS Automatic WCAG contrast font-color depending on element background by Facundo Corradini (@facundocorradini) on CodePen.0

Решение на чистом CSS позволяет добиться того же эффекта, что и JavaScript, но при этом требует меньшего объема кода.

Поддержка браузерами

IE не поддерживает CSS-переменные. Edge не поддерживает абсурдные декларации вроде opacity: 3 и просто отбрасывает их как ошибочные.

Другие современные браузеры должны поддерживать это решение.

Оставить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *