Считайте HSL моделью редактирования sRGB
CSS Color 4 определяет hsl() как способ задать цвет sRGB через hue, saturation и lightness. Это не расширенный цветовой охват и не перцептивно равномерное пространство. Браузер преобразует координаты в красный, зелёный и синий каналы. Поэтому HSL удобен для ручной настройки и совместим с обычными HEX и rgb(), но итог следует проверять по разрешённым каналам sRGB. Именно они нужны для сравнения, хранения и расчёта контраста.
- Hue — угол вокруг цилиндра HSL: 0deg соответствует красному, 120deg — зелёной первичной sRGB, 240deg — синей первичной sRGB.
- Saturation 0% даёт ахроматический результат, а 100% — максимальную насыщенность HSL при выбранных hue и lightness.
- Lightness 0% превращается в чёрный, 100% — в белый, а 50% является серединой модели, но не гарантией средней воспринимаемой яркости.
- Alpha — отдельный компонент. Он меняет композицию с фоном, но не сохранённые координаты hue, saturation и lightness.
- HEX, rgb() и hsl() способны описывать один и тот же цвет в пределах sRGB, хотя предлагают разные способы редактирования.
Записывайте синтаксис без двусмысленности
Современный CSS разделяет компоненты HSL пробелами, а alpha помещает после косой черты. Старый вариант с запятыми поддерживается ради совместимости, однако смешивать две грамматики нельзя. Hue принимает угол; для документации токенов явный deg обычно понятнее. Углы циклические: 0deg, 360deg и 720deg обозначают один hue. Если alpha не указан, цвет полностью непрозрачен.
:root {
--accent: hsl(210deg 100% 40%);
--accent-translucent: hsl(210deg 100% 40% / 60%);
}
/* Допустимый legacy-синтаксис для совместимости. */
.legacy-example {
color: hsl(210, 100%, 40%);
}| Запись HSL | Итоговый HEX | Что показывает пример |
|---|---|---|
| hsl(0 100% 50%) | #FF0000 | 0deg — красный |
| hsl(360 100% 50%) | #FF0000 | Hue зацикливается |
| hsl(120 100% 50%) | #00FF00 | Зелёная первичная sRGB |
| hsl(240 100% 50%) | #0000FF | Синяя первичная sRGB |
| hsl(210 0% 50%) | #808080 | Hue не влияет на ахроматический результат |
Насыщенность меняет цветность, а не прозрачность
Зафиксируем hue на 210deg и lightness на 50%, после чего уменьшим saturation с шагом 25 пунктов. Цвет приближается к ахроматической оси, но остаётся полностью непрозрачным. Это не то же самое, что alpha: полупрозрачный синий показывает неизвестный фон, а сниженная HSL-насыщенность уже даёт новый непрозрачный триплет sRGB. Таблица рассчитана алгоритмом CSS Color 4; итоговые 8-битные каналы округлены до ближайшего целого.
| Насыщенность | HSL | Итоговый HEX | Итоговый RGB |
|---|---|---|---|
| 100% | hsl(210 100% 50%) | #0080FF | 0 128 255 |
| 75% | hsl(210 75% 50%) | #2080DF | 32 128 223 |
| 50% | hsl(210 50% 50%) | #4080BF | 64 128 191 |
| 25% | hsl(210 25% 50%) | #60809F | 96 128 159 |
| 0% | hsl(210 0% 50%) | #808080 | 128 128 128 |
Шкала lightness может потребовать смены foreground
Теперь оставим hue 210deg и saturation 100%, меняя только HSL lightness. Последовательность генерируется просто, но один цвет текста подходит не всем шагам. Контраст вычислен для округлённого HEX по формуле относительной яркости sRGB из WCAG. Для обычного текста уровень AA требует не менее 4,5:1. Выбор чёрного или белого ниже означает только проверенную пару, а не сертификацию всей страницы.
| Lightness | Итоговый HEX | Чёрный текст | Белый текст | Выбор AA для обычного текста |
|---|---|---|---|---|
| 20% | #003366 | 1,67:1 | 12,61:1 | Белый |
| 30% | #004D99 | 2,52:1 | 8,33:1 | Белый |
| 40% | #0066CC | 3,77:1 | 5,57:1 | Белый |
| 50% | #0080FF | 5,53:1 | 3,80:1 | Чёрный |
| 60% | #3399FF | 7,14:1 | 2,94:1 | Чёрный |
| 70% | #66B3FF | 9,46:1 | 2,22:1 | Чёрный |
| 80% | #99CCFF | 12,44:1 | 1,69:1 | Чёрный |
- В этой конкретной шкале foreground меняется между lightness 40% и 50%.
- Hover или selected с тем же hue всё равно требуют отдельных проверок текстового и нетекстового контраста.
- Не округляйте коэффициент до сравнения с 4,5:1; округлять можно только показанное пользователю значение.
- При наличии alpha, градиента, изображения или overlay сначала определите итоговые отображаемые цвета.
HSL lightness не равна воспринимаемой светлоте
Название lightness звучит как характеристика восприятия, но в HSL она выводится из крайних гамма-кодированных каналов sRGB. CSS Color 4 прямо предупреждает: эта координата не передаёт визуальную светлоту одинаково для разных hue. Насыщенные жёлтый и синий имеют HSL lightness 50%, однако их относительная яркость WCAG и подходящие foreground различаются радикально. Равные повороты hue тоже выглядят неравномерно: одни участки круга визуально сжаты, другие растянуты.
| Цвет | HSL | HEX | Относительная яркость | Чёрный текст | Белый текст |
|---|---|---|---|---|---|
| Жёлтый | hsl(60 100% 50%) | #FFFF00 | 92,78% | 19,56:1 | 1,07:1 |
| Синий | hsl(240 100% 50%) | #0000FF | 7,22% | 2,44:1 | 8,59:1 |
Избегайте шести частых ошибок HSL
- Не считайте HSL широкогамутным форматом: CSS hsl() приводится к sRGB и не сохраняет цвет Display P3 за пределами sRGB.
- Не храните значимый hue у серого с saturation 0%: угол не способен изменить отображаемый результат.
- Не называйте изменение HSL lightness тинтом или затемнением, если проект не определил эти слова именно так.
- Не подменяйте saturation прозрачностью: первая меняет непрозрачный цвет, вторая — композицию с фоном.
- Не ждите равных визуальных расстояний от одинаковых шагов hue или lightness: HSL не перцептивно равномерен.
- Не округляйте HSL многократно с перезаписью канонического RGB: дробные преобразования накапливают сдвиг каналов.
Есть и особенность сериализации. Браузер может вернуть разрешённый цвет через rgb(), хотя автор написал hsl(), потому что использованное значение находится в sRGB. В тестах сравнивайте итоговые каналы или заранее нормализованную форму, а не исходную строку. В дизайн-токенах явно укажите, являются ли координаты HSL каноническим редактируемым источником или лишь производным представлением.
Публикуйте HSL-токены вместе с проверяемым результатом
Практичная система токенов может использовать HSL, не выдавая каждый числовой шаг за безопасный. Храните семантические имена, координаты и разрешённые значения sRGB, а затем проверяйте каждую реальную пару foreground/background. В примере белый используется на action с lightness 40%, потому что коэффициент равен 5,57:1. Для полупрозрачного disabled-токена вывод о контрасте невозможен без известного фона.
:root {
--action: hsl(210 100% 40%); /* #0066CC */
--action-hover: hsl(210 100% 32%);
--action-disabled: hsl(210 20% 55% / 60%);
--on-action: #FFFFFF; /* 5,57:1 на --action */
}
.action {
color: var(--on-action);
background: var(--action);
}
.action:hover {
background: var(--action-hover);
}- Храните одно каноническое значение и достаточную точность для воспроизведения разрешённых каналов.
- Документируйте изменяемую координату и семантическую роль полученного токена.
- Экспортируйте готовый HSL и разрешённый HEX или RGB для проверки между инструментами.
- Отдельно проверяйте default, hover, active, focus, selected, disabled, светлую и тёмную темы.
- Считайте контраст по итоговым скомпозированным цветам и сохраняйте неокруглённый коэффициент в аудите.
- Используйте OKLCH или другое подходящее пространство, когда важнее перцептивно ровные шаги, а не знакомые регуляторы HSL.
- Ссылайтесь на метод преобразования и спецификацию, чтобы будущие изменения генератора можно было объяснить.
Первоисточники
Фактические утверждения статьи проверены по этим опубликованным спецификациям.