Зашитый цвет: значение, записанное прямо в код, #2563eb вместо токена. Такой элемент тихо выпадает из дизайн-системы. Он не последует за ребрендингом и не перекрасится в тёмной теме. Здесь собраны команды, которые за минуты находят зашитые цвета в React-кодовой базе, случаи ложных срабатываний и способ остановить новые на ревью.
Коротко
- Ищите пять видов цвета: hex, цветовые функции, именованные цвета, токены палитры и лазейки фреймворков вродеbg-[#…]в Tailwind.
- Прежде чем считать, исключите сгенерированный код и комментарии, иначе цифры соврут.
- Превратите поиск в правило линтера, чтобы счётчик только снижался.
- Зашитый цвет не всегда ошибка. Разрешайте исключения, но с записанной причиной.
Что считается зашитым цветом
CSS принимает больше синтаксисов цвета, чем обычно помнят. CSS Color 4 перечисляет hex из 3, 4, 6 или 8 цифр, rgb() и rgba(), hsl() и hsla(), hwb(), lab(), lch(), oklab(), oklch() и color() (CSS Color Module Level 4). Сверху ещё около 150 именованных цветов (MDN: named-color).
В React-кодовой базе они прячутся в пяти местах:
Последние два легко пропустить. Примитивный токен палитры вроде gray.100 выглядит как токен, но ведёт себя как зашитое значение: в любой теме он один и тот же. Квадратные скобки Tailwind «на лету генерируют класс с любым произвольным значением» (Tailwind: arbitrary values). Это удобно, но тему такой класс обходит целиком.
Команды поиска для каждого вида цвета
ripgrep быстро ищет по всему репозиторию и учитывает .gitignore, поэтому результаты сборки по умолчанию не попадают в выдачу. Его стандартный движок регулярных выражений не умеет lookbehind, поэтому шаблон для hex построен на классе символов. Начните с двух шаблонов, которые ловят большинство случаев:
# Hex colours, not preceded by a letter, & or ( (skips – and issue refs like (#619))
rg -n -e '(^|[^&\w(])#[0-9a-fA-F]{3,8}\b' src
# Colour functions (color( is left out: it matches too many JavaScript calls)
rg -n -e '\b(rgba?|hsla?|hwb|lab|lch|oklab|oklch)\(' srcЗатем шаблоны, специфичные для React:
# Raw palette tokens in style props (adjust the palette names to your theme)
rg -n -e '["'"'"'`](gray|red|blue|green)\.\d{2,3}["'"'"'`]' src
# Tailwind arbitrary colours
rg -n -o -e '\b(bg|text|border|fill|stroke)-\[#[0-9a-fA-F]{3,8}\]' src
# Named colours in JS style props (a bare word search drowns in noise; plain CSS needs a Stylelint rule)
rg -n -e '\b(color|background|backgroundColor|bg|borderColor|fill|stroke)\s*[:=]\s*\{?\s*["'"'"'](white|black|red|blue|green|gray|grey)["'"'"']' srcМы проверили их на небольшом тестовом проекте, где было по одному случаю каждого вида. Пять команд нашли все зашитые значения и ещё три ложных срабатывания. Их убирает следующий раздел.
Уберите шум
Сырой счёт почти всегда завышен. Его раздувают три источника:
- Комментарии. Комментарий вроде
// Fixes the bug from #619совпадает с шаблоном для hex. Мы наткнулись ровно на это в собственной проверке CSS:(#619)внутри комментария засчитался как цвет. Отбросьте строки комментариев до подсчёта. - Сгенерированный код. Компоненты иконок, экспортированные из Figma, несут исходные цвета. Когда мы впервые измеряли демо-репозиторий, иконки, выгруженные нашей же синхронизацией, дали около 590 зашитых цветов. С кодом всё было в порядке, ошибалась метрика. Исключите сгенерированные папки glob-шаблоном.
- Сама тема. Файл, который связывает смысловые токены с палитрой, обязан упоминать палитру. Исключите его по имени.
Всё вместе, со счётом по файлам:
rg -n --no-heading -g '!src/icons/**' -g '!src/theme.ts' \
-e '(^|[^&\w(])#[0-9a-fA-F]{3,8}\b' \
-e '\b(rgba?|hsla?|hwb|lab|lch|oklab|oklch)\(' src \
| rg -v '^[^:]+:\d+:\s*(//|/\*|\*)' \
| cut -d: -f1 | sort | uniq -c | sort -nrНа выходе получается ранжированный список файлов: начинайте сверху. Фильтр отбрасывает только строки, целиком занятые комментарием, а комментарии в конце строки по-прежнему попадают в счёт. Поэтому просмотрите первые результаты, прежде чем доверять итогу.
Решите, что разрешено
Не каждый зашитый цвет ошибка. Точный цвет логотипа бренда, белый фон шаблона письма, чёрный фон под видеоплеером: это значения, а не роли. Проблема в исключении, которого никто не видит.
Наше правило простое. Исключение получает комментарий с причиной на той же строке или строкой выше, и проверка его пропускает.
// design-tokens-allow: many email clients don't support CSS variables
const EMAIL_CANVAS = "#ffffff";Ревьюер прочитает все исключения одним поиском, а на вопрос «почему здесь зашитый цвет?» ответ есть прямо в коде.
Остановите новые на ревью
Поиск показывает, где вы сейчас. Правило линтера не даёт откатиться назад.
- Stylelint. Встроенное правило
color-no-hexделает то, что обещает его название. Плагинstylelint-declaration-strict-valueидёт дальше. Для перечисленных свойств, напримерcolor,fillиstroke, он требует переменную, функцию или ключевое слово. - ESLint. Правило Atlassian
ensure-design-token-usageотмечает зашитые цвета и сырые отступы в JavaScript и требует функциюtoken(). Оно привязано к токенам Atlassian, но показывает, каким может быть правило для ваших собственных токенов. - Скрипт в CI. Если стили живут в пропсах, короткий скрипт на шаблонах выше часто оказывается самым быстрым путём. Наш запускается как
pnpm design:checkв CI на каждом pull request и перед каждым коммитом.
Включите правило в режиме предупреждений и исправьте верхние файлы из своего списка. Затем переключите его в режим ошибки. Дальше счётчик должен только снижаться.
Место зашитых цветов в проверке здоровья
Зашитые цвета относятся к обходу системы: это код, который идёт мимо системы, даже когда нужный токен существует. Их подсчёт входит в четыре сигнала из нашего руководства по измерению здоровья дизайн-системы. Эту часть аудита можно провести сегодня же вечером. Аудит дизайн-системы за один вечер ставит её рядом с остальными проверками.



