
Наверняка многие из вас бывали в ситуации, когда нужно быстро подобрать цвета для оформления, а дизайнер занят более важными задачами, в плохом настроении или в отпуске. Задача несложная, но иногда ответа приходится ждать по несколько дней.
Меня зовут Эмиль Фролов, я техлид в команде внутренних сервисов в ДомКлике. В этой статье я расскажу, как родилась идея библиотеки, которая теперь сильно экономит время при выборе цветов. Решение простое, но очень полезное, берите на вооружение.
Особенно, если нужно подобрать цвета для тёмной схемы.
Задача
В ДомКлик есть корпоративный портал, каждый раздел которого имеет свою цветовую схему. Раньше для создания нового раздела каждый раз приходилось мучить дизайнеров и просить их подобрать новый набор цветов и перенести в интерфейс. Получалось огромное количество лишнего кода, тратили кучу времени на переписку, ожидание и согласования. Очень хотелось упростить и ускорить весь процесс.
Поискали в интернете готовые решения, но ничего подходящего не нашлось. И тогда решили написать библиотеку: вводишь в неё цвет (брендовый), который даёт дизайнер, а библиотека подбирает еще несколько подходящих цветов. Также нам хотелось, чтобы библиотека генерировала ещё и тёмные цветовые схемы.
Это не рецепт счастья, а, скорее, идея, которую каждый сможет развить в рамках своего проекта. Небольшую демку можно посмотреть тут.
Идея и решение
Подумали, как это можно сделать. Начали с алгоритма генерирования цветов на основе базового. В этих ваших интернетах снова ничего готового не нашли. Зато нашли библиотеку, которая может менять разные параметры цвета.
Многие знают что есть множество разнообразных алгоритмов подбора цветов:

Расписывать их не вижу смысла, это уже сделали сотни раз до меня. Но есть несколько ключевых моментов:
- Для нас они избыточны.
- Хотелось подбирать цвета под себя.
Поэтому слившиеся в едином порыве дизайнер и разработчик решили единожды подобрать схему вручную.
Для начала описали базовые цвета:
export const getColors = (projectColor, inverse) => { ... const BASE_SUCCESS = '#00985f'; const BASE_WARNING = '#ff9900'; const BASE_PROGRESS = '#fe5c05'; const BASE_ALERT = '#ff3333'; const BASE_SYSTEM = '#778a9b'; const BASE_NORMAL = '#dde3e5'; const BASE_WHITE = '#ffffff'; const BASE_BLACK = '#000'; const TYPO_BASE_BLACK = '#242629'; const TYPO_LINK = '#33BDFF'; ... }
Отталкиваясь от этого, можно начать творить. Получаем объекты для работы с базовыми цветами.
import color from 'color-js'; export const getColors = (projectColor, inverse) => { ... const baseWhite = color(BASE_WHITE); const baseBlack = color(BASE_BLACK); const baseTypoBlack = color(TYPO_BASE_BLACK); ... }
Думаю, нет смысла полностью описывать весь подбор цветов, но для примера приведу пару строчек:
export const getColors = (projectColor, inverse) => { ... const bgBrand = baseProject; const bgHard = baseColor.setLightness(0.4); const bgSharp = baseColor.setLightness(0.18); const bgStripe = baseColor.setLightness(0.1); const bgGhost = baseColor.setLightness(0.07); ... }
После того, как мы закончили подбирать основные цвета, перед нами встала следующая проблема.
А каким цветом отображать текст на элементах?
Задача оказалась не такой сложной, как казалось. Для её решения из hex-значения нам нужно получить яркость элемента. Сделали мы это двумя вспомогательными функциями. Первая переводит hex в RGB:
const hexToRgb = (hex) => { const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; const newHex = hex.replace(shorthandRegex, ( magenta, red, green, blue ) => red + red + green + green + blue + blue); const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(newHex); return result ? { red: parseInt(result[1], 16), green: parseInt(result[2], 16), blue: parseInt(result[3], 16) } : null; };
Вторая функция из RGB получает яркость фона и решает, темным будет цвет или светлым:
export const getBgTextColor = (bgColor) => { const rgb = hexToRgb(bgColor); const light = (rgb.red * 0.8 + rgb.green + rgb.blue * 0.2) / 510 * 100; return light > 70 ? '#000000' : '#ffffff'; };
Вы можете подумать, что теперь всё готово к следующему этапу. Но нет, мы ведь ещё хотим поддержку тёмной темы из коробки? Да! Хотим!
Наверное, вы обратили внимание, что мы передаем в нашу функцию флаг
inverse. Давайте несколько поменяем наш код с учётом этого флага:import color from 'color-js'; export const getColors = (projectColor, inverse) => { ... const BASE_SUCCESS = '#00985f'; const BASE_WARNING = '#ff9900'; const BASE_PROGRESS = '#fe5c05'; const BASE_ALERT = '#ff3333'; const BASE_SYSTEM = '#778a9b'; const BASE_NORMAL = '#dde3e5'; const BASE_WHITE = '#ffffff'; const BASE_BLACK = '#000'; const TYPO_BASE_BLACK = '#242629'; const TYPO_LINK = '#33BDFF'; ... const baseWhite = color(BASE_WHITE); const baseBlack = color(BASE_BLACK); const baseTypoBlack = color(TYPO_BASE_BLACK); const baseColor = inverse ? baseWhite : baseBlack; const typoColor = inverse ? baseWhite : baseTypoBlack; ... const bgHard = inverse ? baseColor.setLightness(0.4) : baseColor.lightenByAmount(0.85); const bgSharp = inverse ? baseColor.setLightness(0.18) : baseColor.lightenByAmount(0.95); const bgStripe = inverse ? baseColor.setLightness(0.1) : baseColor.lightenByAmount(0.96); const bgGhost = inverse ? baseColor.setLightness(0.07) : baseColor.lightenByAmount(0.99); ... }
Вот и всё. Можем отдать список цветов:
return { ... // BG 'color-bg-hard': bgHard.toString(), 'color-bg-sharp': bgSharp.toString(), 'color-bg-stripe': bgStripe.toString(), 'color-bg-ghost': bgGhost.toString(), 'color-bg-primary': bgDefault.toString(), ... }
За два дня я с помощью нашего дизайнера создал библиотеку, которая на выходе даёт цветовую палитру для тёмной и светлой темы.
Следующий вопрос: как этим пользоваться?
Очень просто. Для внедрения сгенерированной цветовой схемы мы применили вставку CSS-переменных через блок стилей. Это позволяет избегать конфликтов с CSS-переменными, которые используют другие CSS-библиотеки.
const colors = getColors(color, themeKey === 'dark'); const colorsVars = Object.keys(colors).map((key) => `--${key}: ${customColors[key]}`).join(';'); const link = document.createElement('style'); const headTag = document.getElementsByTagName('head')[0]; link.type = 'text/css'; link.id = 'project-theme-scope'; const stylesBody = `:root {${colorsVars}}`; link.innerText = stylesBody; headTag.append(link);
А теперь самое вкусное. Внимание-внимание! Сейчас с помощью нескольких строчек когда мы добавим поддержку тёмной темы для половины элементов:
body { background: var(--color-bg-ghost); color: var(--color-typo-primary); }
Получилась библиотека, в которую мы передаем основной брендовый цвет и получаем набор CSS-переменных, которыми затем пользуемся для раскрашивания нашего проекта.
Больше всего времени ушло на подбор цветовой схемы. Пришлось вручную перебирать кучу разных параметров, чтобы цвета сочетались друг с другом. Зато теперь на каждой итерации подбора цветов для новых разделов портала мы экономим по несколько дней.
Поскольку дизайнер принимал участие в создании алгоритма, ещё не было случая, чтобы он был недоволен сгенерированными библиотекой цветами. Да и цветовые схемы не слишком велики, в них трудно ошибиться.
Ещё раз подчеркну, что мы не претендуем на единственно верное решение. Эта идея реализована, и она работает хорошо. Я постарался донести до вас основные моменты, а детали и масштабы реализации зависят только от вас и вашего проекта.
Спасибо за внимание.