Opium.Fill — стандартизация цветовой схемы глазами программиста

Привет. Сегодня покажу вам цветовую схему, которой пользуюсь последние два года. Она была придумана, чтобы на проблемном проекте избавиться от огромного количества переменных в CSS. А потом оказалось, что эти принципы можно применить почти к любому проекту.

Opium.Fill — стандартизация цветовой схемы глазами программиста

В общем, попробую объяснить, как дизайнеры используют цвет в UI и как всё это можно «типизировать», не вгоняя дизайнеров в жёсткие рамки. Приведу примеры реализации на React JS (для разработчика) и в Figma (для дизайнера). Привязки к React и Figma у схемы нет, просто мне в них привычнее.

В схеме нет ничего хитрого и уникального (может, только название). Все идеи висят в воздухе. Можно воспринимать её как мой best practice по работе с цветом в приложениях. Opium.Fill — это общие принципы, совмещённые с любовью давать всему подряд имена.

Систему можно использована совместно с Material Design.

Статья написана для front-end-разработчика и немного для дизайнера.

Оглавление

1. Какие проблемы решаем

1.1. 50 оттенков серого

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

1.2. Дизайнер играется с цветами

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

В таком случае накапливается много переменных цвета. Ведь разработчик не понимает, нужно ли удалять старые цвета, или они ещё где-то используются. Из тех проектов, что я видел, рекорд — 273 переменных только для цвета.

Похожая ситуация может встречаться на всех проектах, где работа идёт по Agile и дизайн меняется одновременно с разработкой.

1.3. Ночные темы и брендирование дизайна

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

Например, вы работаете над созданием CRM. И ваша CRM даёт клиенту возможность подстроить свою цветовую схему под фирменный стиль клиента. Если у вас нет понятной цветовой схемы, то сделать это будет сложно.

А что если вам нужна тёмная тема для приложения? Тогда предложение «Василий, давайте возьмём наши 273 переменные и систематизируем их» приводит к тому, что Василий сломал форму на 10-м шаге оформления заявки, поругался с разработчиком из соседнего департамента и через неделю сошёл с ума.

2. Идеология Opium.Fill

2.1. Не мешай дизайнеру работать

Opium.Fill придуман, чтобы «расшифровывать» дизайн, а не грузить дизайнера. Дизайнеру необязательно знать о существовании Opium.Fill, чтобы делать всё по схеме.

Не стоит навязывать дизайнеру цветовую схему и лучше сначала подождать, пока он закончит рисовать основную концепцию приложения. Только после этого полезно показать ему, если что-то не стыкуется с цветовой схемой, и уточнить, можно ли эти места поправить. На 9 из 10 таких вопросов дизайнеры говорят: «Пф, это не проблема, давай заменим» или «Ой, а это вообще мой косяк, спасибо, что заметил».

2.2. Определяй цвета «на глаз»

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

Opium.Fill — стандартизация цветовой схемы глазами программиста

2.3. Не расстраивайся, если получается не всё

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

3. Базовые допущения

3.1. Каждому цвету — по паре

Мы считаем, что у каждого цвета есть по два «воплощения». Первое — насыщенное (условно Strong). Второе — ненасыщенное (условно Weak). Если мы видим синий цвет, то помимо того, что он синий, мы должны определить его как Strong или Weak. Он насыщенный или ненасыщенный?

3.2. Делим цвета по функциональности

Забываем про «sky-blue», «gold», «jet-black» и тому подобное в названиях цветов. Название цвета должно отражать его функциональность, а не его hex. Теперь мы работаем с такими названиями: Base, Faint, Accent, Complement, Critic, Warning, Success.

3.3. Три блока

Блок № 1 — самый важный. Блок № 3 — самый неважный. Ниже я опишу каждый из блоков. Это разделение нужно, чтобы менее значимые для рисования интерфейса цвета нас меньше отвлекали.

4. Блок № 1. Родительские цвета

4.1. Названия

Вернёмся к нашим новым названиям цветов. Base, Faint, Accent, Complement, Critic, Warning, Success. Давайте каждый по очереди разберём.

Base

Это чёрный и белый цвета. Или те цвета, которые схожи с ними до степени смешения. Они — базовые для текста и бэкграунда.

Opium.Fill — стандартизация цветовой схемы глазами программиста

Faint

Так мы называем оттенки серого. Какой-нибудь второстепенный текст или сероватый фон — это Faint. Чёрный цвет с прозрачностью (если он воспринимается как серый) тоже включается сюда.

Opium.Fill — стандартизация цветовой схемы глазами программиста

Accent

Это главный корпоративный цвет или цвет, который выделяет наиболее важные элементы интерфейса. К примеру, если смотреть на российские банки, то: Сбербанк — зелёный, ВТБ — синий (или красный, как посмотреть), Тинькофф — жёлтый, Альфа — красный. Давайте для удобства в нашей таблице под Accent будем иметь в виду оттенки синего.

Opium.Fill — стандартизация цветовой схемы глазами программиста

Complement

Это дополнительный акцентный цвет. Не у всех он есть. Посмотрим на Airbnb — я бы сказал, что это тёмно-зелёный:

Opium.Fill — стандартизация цветовой схемы глазами программиста

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

Opium.Fill — стандартизация цветовой схемы глазами программиста

Critic

Цвет для выделения ошибок и прочей крайне существенной информации. Обычно что-то красное.

Opium.Fill — стандартизация цветовой схемы глазами программиста

Warning

Если вы хотите разделять критичную информацию и другую, тоже важную, но не смертельно, понадобится Warning. Обычно это что-то вроде жёлто-оранжевого.

Opium.Fill — стандартизация цветовой схемы глазами программиста

Success

Иногда успешно выполненное действие достаточно показать цветом Accent. Но если Accent какого-то необычного цвета (красного) или ещё по каким-то причинам хочется ввести новый цвет, то вот вам Success. Скорее всего, он окажется зеленоватым.

Opium.Fill — стандартизация цветовой схемы глазами программиста

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

4.2. Цветовые семьи

Вы, наверное, уже обратили внимание, что когда я говорю про цвет, то не указываю конкретное значение, а использую слова «оттенки серого», «что-то красное», «зеленоватый» и т. д. Это не просто так. Давайте введём понятие «цветовая семья» (Color Family).

Как я говорил выше, мы делим цвета по парам. Не может существовать просто Accent. Обязательно есть Accent Strong и Accent Weak. Это всегда именно два цвета, которые формируют основу семьи. Как папа и мама.

Opium.Fill — стандартизация цветовой схемы глазами программиста

Представим на время, что наши цвета — это семейная пара. Допустим, не важно, какого пола родители, главное, чтобы один был сильным (Strong), а второй — слабым (Weak). И примем также, что сила и слабость — черты характера, как вспыльчивость и спокойствие.

Так вот. Папа — вспыльчивый. Он часто нервничает в пробках и пинает ежей в лесу. Мама — спокойная. Она работает психологом и играет в покер. Это и есть основа нашей цветовой схемы.

Цвета Base у нас уже поделены (чёрный и белый). Поделим остальные на семьи:

Opium.Fill — стандартизация цветовой схемы глазами программиста

5. Блок № 2. Подмены

5.1. Контекст

Посмотрим на Bitbucket. Тут дизайнер посчитал, что тот синий цвет, который на подложке слева, темноват для текста. И поэтому он осветлил его. Теперь весь текст хоть и выглядит синим (как боковое меню), на самом деле имеет другое значение в hex:

Opium.Fill — стандартизация цветовой схемы глазами программиста

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

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

Opium.Fill — стандартизация цветовой схемы глазами программиста

Мы добавили ячейки подмены только для цветов Strong. Цвета Weak в блоке № 2 показывают, как этот цвет выглядит на тёмном бэкграунде. Пока не будем их трогать, но в конце ещё к ним вернёмся.

Если подмена не заполнена, значит, цвет для текста, иконок и всего остального берётся из блока № 1.

Примечание. Мы можем подменять цвета только в примитивных элементах дизайна. Этот набор взят почти 1:1 из графических редакторов, которыми пользуются дизайнеры. Там есть возможность нарисовать прямоугольник (бэкграунд), добавить блок текста, нарисовать линию или добавить какую-то необычную фигуру, например звёздочку (считай иконку).

5.2. Fancy

Есть ещё один особенный вид подмены — подмена на градиент. Назовём градиенты словом Fancy — контекст по «особому поводу». Fancy един для всех. Не может быть отдельного градиента для текста, иконок и т. д. (в теории, конечно, может, но не стоит из-за такого редкого случая делать таблицу сложнее). Выделим для Fancy место внизу таблицы:

Opium.Fill — стандартизация цветовой схемы глазами программиста

Мы подготовились к изменениям в зависимости от контекста, дизайнеры 100% это делают. Пустые ячейки дают нам запас гибкости. Иногда подмены приходится делать чаще, иногда реже. Если блок № 2 остаётся почти пустым — это нормально (дизайнер тоже старается сократить количество цветов).

6. Блок № 3. Сдвиги

Иногда цвет нужно немного затемнить или осветлить. Но это не потому, что поменялся контекст, а просто у каждого цвета есть два дополнительных состояния: «потемнее» и «посветлее». Чаще всего дизайнеры это используют, чтобы сделать реакцию на наведение мышкой. Такое изменение цвета мы называем сдвигом (Shift). Можно сделать сдвиг вверх (темнее) или сдвиг вниз (светлее).

Opium.Fill — стандартизация цветовой схемы глазами программиста

Пустая таблица теперь выглядит так:

Opium.Fill — стандартизация цветовой схемы глазами программиста

7. Инверсия цвета

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

Opium.Fill — стандартизация цветовой схемы глазами программиста

Там есть текст на синей плашке, который субъективно воспринимается как Faint. А ещё есть кружки под иконками, которые я бы тоже определил в семью Faint.

Если бы плашка была светлой, то оба этих элемента, скорее всего, были бы просто серыми. Но плашка тёмная. В нашей таблице нет подходящих цветов, их можно добавить. Как раз для этого понадобится колонка Weak в блоке № 2.

Для данного макета таблица выглядит так:

Opium.Fill — стандартизация цветовой схемы глазами программиста

Обратите внимание, что теперь линии будут нарисованы более светлым Faint Strong, это тоже отразилось в таблице. Ещё добавились градиенты для Accent и сдвиги для родительских Faint (они пригодятся, чтобы нарисовать поле поиска).

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

8. Использование

8.1. CSS, React

Попробуем нарисовать такую кнопку

Opium.Fill — стандартизация цветовой схемы глазами программиста

В чистом CSS переменные можно организовать так:

:root { /* Родительские цвета */ --base-strong: #000; --base-weak: #fff; --faint-strong: #8994A6; --faint-weak: #F6F8FB; --accent-strong: #0070FF; --accent-weak: #EBF4FF; --complement-strong: #8889E2; --complement-weak: #EEECFD; --critic-strong: #F74545; --critic-weak: #FDEDED; --warning-strong: #F8AE4F; --warning-weak: #FCEBCF; --success-strong: #27AE60; --success-weak: #DEF8E9; /* Cдвиги родительских цветов */ --faint-strong-down: #A5ADBB; --faint-weak-up: #ECEEF5; } /* Подмены */ /* Контекст добавляем в виде классов */ .back { --faint-weak: rgba(255, 255, 255, 0.15); } .text { --faint-weak: rgba(237, 241, 247, 0.5); } .line { --faint-strong: #EDEFF2; } .icon {} .fancy { --accent-strong: linear-gradient(132deg, #3F89EE, #5447FF); /* Сдвиги */ --accent-strong-down: linear-gradient(132deg, #448FF3, #594CFF); } /* Рисуем кнопку */ .button { background: var(--accent-strong); color: var(--base-weak); /* Дальше просто оформительство */ } .button:hover { background: var(--accent-strong-down); }

И тогда, чтобы создать кнопку в HTML, нужно будет добавить такую разметку:

<button class="fancy button"> <div class="text"> Hello Button </div> </button>

В CSS нет средств, чтобы нативно отследить контекст, и тогда придётся его указывать вручную через классы. Можно, конечно, пытаться понять контекст по используемым тегам, но это решение на любителя.

Ещё способ — определять контекст через data-атрибуты вместо классов:

<button data-context="fancy" class="button"> <div data-context="text"> Hello Button </div> </button>

Другой вариант использования data-атрибутов — сразу задавать через них конечный цвет:

<button data-back-fancy data-back-color="accent-strong" data-text-color="base-weak" class="button" > Hello Button </button>

Тогда CSS должен содержать следующее:

[data-back-color="accent-strong"] { background-color: #0070FF; } [data-back-fancy][data-back-color="accent-strong"] { background-image: linear-gradient(132deg, #3F89EE, #5447FF); } [data-text-color="base-weak"] { color: #fff; } /* И так далее для остальных цветов */

На React код может выглядеть так:

class Button extends React.Component { render() { return ( <Box fill="accentStrong" fancy className="button"> <Font fill="baseWeak">Hello Button</Font> </Box> ) } } // Или немного покороче class Button extends React.Component { render() { return ( <Box.Accent fancy className="button"> <Font>Hello Button</Font> </Box.Accent> ) } } // Box и Font — базовые компоненты для примитивов, по ним мы понимаем контекст // Во втором примере даже можно не указывать Strong или Weak (а для текста вообще цвет не указывать) // Таблица часто позволяет компонентам подбирать цвета автоматически // Хавер тоже можно иногда высчитать автоматически // Как реализовать базовые компоненты, описывать не буду // Думаю, вы сами тут разберётесь // Да и к тому же, это не обязательно для Opium.Fill // Здесь показан только один из способов реализации // В классе 'button' уже никак не указываем цвет // С цветом мы разобрались на примитивах

Чтобы не быть привязанным к CSS (вы же не только для браузера делаете приложения), переменные цвета можно хранить в json:

{ "color": { "parents": { "baseStrong": "#000", "baseWeak": "#fff", "faintStrong": { "default": "#8994A6", "shiftDown": "#A5ADBB" }, "faintWeak": { "default": "#F6F8FB", "shiftUp": "#EDEFF2" }, "accentStrong": "#0070FF", "accentWeak": "#EBF4FF", "complementStrong": "#8889E2", "complementWeak": "#EEECFD", "criticStrong": "#F74545", "criticWeak": "#FDEDED", "warningStrong": "#F8AE4F", "warningWeak": "#FCEBCF", "successStrong": "#27AE60", "successWeak": "#DEF8E9" }, "context": { "back": { "faintWeak": "rgba(255, 255, 255, 0.15)" }, "text": { "faintWeak": "rgba(237, 241, 247, 0.5)" }, "line": { "faintStrong": "#EDEFF2" }, "icon": {}, "fancy": { "accentStrong": { "default": "linear-gradient(132deg, #3F89EE, #5447FF)", "shiftDown": "linear-gradient(132deg, #448FF3, #594CFF)" } } } } }

8.2. Цветовая схема в Figma

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

Opium.Fill — стандартизация цветовой схемы глазами программиста

9. Когда нет смысла использовать систему

9.1. Необычный дизайнерский проект

Opium.Fill устроен так, что базовые цвета — это чёрный и белый (или те, что субъективно похожи на них до степени смешения). Если нужно много писать синим по красному или постоянно использовать в одном интерфейсе самые разные цвета, то Opium.Fill — не лучшее решение.

9.2. Маленький проект

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

9.3. Вы уже пользуетесь чем-то другим

Если ваша команда использует Material Design (и не только) и все всем довольны, то нет смысла менять или корректировать то, что работает.

10. Критика

Приведу несколько часто озвучиваемых проблем с Opium.Fill. На часть из претензий попробую ответить. Можно считать это открытой темой. Если кто-то столкнётся со сложностями, буду рад, если поделитесь со мной.

10.1. Переменных всё равно много

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

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

Если в вашей таблице накапливается больше 50 штук, то, наверное, что-то идёт не так и схема, к сожалению, не помогает это исправить.

10.2. Для закраски графиков придётся сильно расширять таблицу

Если у вас много графиков или нужно покрасить категории товаров в разные цвета (а их, например, 20 штук), то придётся добавлять новые цветовые семьи. Семи штук, что я описал выше, не хватит.

10.3. Зачем добавлять подмены, если в теории можно все потребности покрыть сдвигами?

Я исхожу из идеи, что подмены в зависимости от контекста — это более «высокоуровневое» объяснение того, как дизайнер использует цвета. И поэтому подмены проще понять и использовать.

Сдвиги пригодятся для редких цветов, их лучше заполнять в случаях:

  1. Обработки событий (таких как хавер или нажатие)
  2. Когда нужен дополнительный спектр оттенков серого

Заключение

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

Если тебе близка тема управления цветами на проекте, возможно, будет интересно почитать и про другие варианты: Material Design, Atlassian Design

Спасибо

Придумали схему и написали статью — Денис Элиановский, команда JTC

Иллюстрация в шапке — Елена Ефимова

Библиотека на React, использующая Opium.Fill — themeor.opium.pro

6868
20 комментариев

Ещё не успел дочитать материал, но уже выражаю огромный респект автору!

6

Врешь ты все, на VC программистов нет, они все на реддите и хабре

5

Bootstrap, в котором давно продумана такая система цветов вышла из чата

3

Спасибо за комментарий.
В 4 версии бутстрапа появилось что-то похожее.
Но там есть и сходства и отличия:

1) Они используют похожий набор «семей», но у них он гораздо шире (есть info, body и произвольные вроде black-50)

2) Они разделяют цвет на цвет для текста и для бэкграунда, но есть ещё для кнопок, форм, алертов и тд. Это тоже гораздо шире, чем в Opium.Fill

3) В бутстрапе нет концепции деления на strong и weak и нет сдвигов

И есть два принципиальных отличия:

1) Все сущности в Opium.Fill имеют не больше 7 значений (в основном 3-4). Это сделано специально, так как многие исследования доказывают, что человеку трудно держать в коротковременной памяти больше 7 единиц информации. А значит можно предположить, что таблицу Opium.Fill можно быстрее запомнить и целиком держать в голове. Это помогает работать быстрее.

2) В бутстрапе есть также привязка к элементам интерфейса (кнопки, алерты, формы и тд). В Opium.Fill привязка идёт к примитивным элементам дизайна (текст, фон, линия, иконка). Это, на мой взгляд, помогает более гибко подстраиваться под то, что нарисовал дизайнер, т.к. интерфейсы со временем мутируют.

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

3

Прекрасная схема! Нужно пробовать, но субъективно кажется, что это будет отлично работать. Спасибо!

2

Интересное решение, но я правильно понимаю, что это не плагин, не библиотека, а просто продуманная система? 

1

Спасибо. Да, это описание концепции. Но мы ещё опубликовали библиотеку на React, которая использует принципы Opium.Fill

Вот ссылка http://themeor.opium.pro

Для установки через npm:
npm i themeor node-sass

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

Приветствую всех, кто захочет поконтрибьютить)
Если будет сложно разбираться, пишите тикеты на github (https://github.com/opium-pro/themeor), мы стараемся сделать библиотеку максимально понятной и полезной

2