|
| 1 | +--- |
| 2 | +title: "Request for Comments: Color Spaces" |
| 3 | +author: Miriam Suzanne and Natalie Weizenbaum |
| 4 | +date: 2022-09-20 19:00 PST |
| 5 | +--- |
| 6 | + |
| 7 | +There's been a lot of exciting work in the CSS color specifications lately, and |
| 8 | +as it begins to land in browsers we've been preparing to add support for it in |
| 9 | +Sass as well. The first and largest part of that is adding support for *color |
| 10 | +spaces* to Sass, which represents a huge (but largely backwards-compatible) |
| 11 | +rethinking of the way colors work. |
| 12 | + |
| 13 | +Historically, all colors in CSS have existed in the same color space, known as |
| 14 | +"sRGB". Whether you represent them as a hex code, an `hsl()` function, or a |
| 15 | +color name, they represented the same set of visible colors you could tell a |
| 16 | +screen to display. While this is conceptually simple, there are some major |
| 17 | +downsides: |
| 18 | + |
| 19 | +* As monitors have improved over time, they've become capable of displaying more |
| 20 | + colors than can be represented in the sRGB color space. |
| 21 | + |
| 22 | +* sRGB, even when you're using it via `hsl()`, doesn't correspond very well with |
| 23 | + how humans perceive colors. Cyan looks noticeably lighter than purple with the |
| 24 | + same saturation and lightness values. |
| 25 | + |
| 26 | +* There's no way to represent domain- or device-specific color spaces, such as |
| 27 | + the [CMYK] color space that's used by printers. |
| 28 | + |
| 29 | + [CMYK]: https://en.wikipedia.org/wiki/CMYK_color_model |
| 30 | + |
| 31 | +Color spaces solve all of these problems. Now not every color has a red, green, |
| 32 | +and blue channel (which can be interpreted as hue, saturation, and lightness). |
| 33 | +Instead, a every color has a specific *color space* which specifies which |
| 34 | +channels it has. For example, the color `oklch(80% 50% 90deg)` has `oklch` as |
| 35 | +its color space, `80%` lightness, `50%` chroma, and `90deg` hue. |
| 36 | + |
| 37 | +## Color Spaces in Sass |
| 38 | + |
| 39 | +Today we're announcing [a proposal for how to handle color spaces in Sass]. In |
| 40 | +addition to expanding Sass's color values to support color spaces, this proposal |
| 41 | +defines Sassified versions of all the color functions in [CSS Color Level |
| 42 | +4][color-4]. |
| 43 | + |
| 44 | +[a proposal for how to handle color spaces in Sass]: https://github.com/sass/sass/blob/main/proposal/color-4-new-spaces.md |
| 45 | + |
| 46 | +### Rules of Thumb |
| 47 | + |
| 48 | +There are several rules of thumb for working with color spaces in Sass: |
| 49 | + |
| 50 | +* The `rgb`, `hsl`, and `hwb` spaces are considered "legacy spaces", and will |
| 51 | + often get special handling for the sake of backwards compatibility. Colors |
| 52 | + defined using hex notation or CSS color names are considered part of the |
| 53 | + `rgb` color space. Legacy colors are emitted in the most compatible format. |
| 54 | + |
| 55 | +* Otherwise, any color defined in a given space will remain in that space, and |
| 56 | + be emitted in that space. |
| 57 | + |
| 58 | +* Authors can explicitly convert a color's space by using `color.to-space()`. |
| 59 | + This can be useful to enforce non-legacy behavior, by converting into a |
| 60 | + non-legacy space, or to ensure the color output is compatible with older |
| 61 | + browsers by converting colors into a legacy space before emitting. |
| 62 | + |
| 63 | +* The `srgb` color space is equivalent to `rgb`, except that one is a legacy |
| 64 | + space, and the other is not. They also use different coordinate systems, with |
| 65 | + `rgb()` accepting a range from 0-255, and `srgb` using a range of 0-1. |
| 66 | + |
| 67 | +* Color functions that allow specifying a color space for manipulation will |
| 68 | + always use the source color space by default. When an explicit space is |
| 69 | + provided for manipulation, the resulting color will still be returned in the |
| 70 | + same space as the origin color. For `color.mix()`, the first color parameter |
| 71 | + is considered the origin color. |
| 72 | + |
| 73 | +* All legacy and RGB-style spaces represent bounded gamuts of color. Since |
| 74 | + mapping colors into gamut is a lossy process, it should generally be left to |
| 75 | + browsers, which can map colors as-needed, based on the capabilities of a |
| 76 | + display. For that reason, out-of-gamut channel values are maintained by Sass |
| 77 | + whenever possible, even when converting into gamut-bounded color spaces. The |
| 78 | + only exception is that `hsl` and `hwb` color spaces are not able to express |
| 79 | + out-of-gamut color, so converting colors into those spaces will gamut-map the |
| 80 | + colors as well. Authors can also perform explicit gamut mapping with the |
| 81 | + `color.to-gamut()` function. |
| 82 | + |
| 83 | +* Legacy browsers require colors in the `srgb` gamut. However, most modern |
| 84 | + displays support the wider `display-p3` gamut. |
| 85 | + |
| 86 | +### Standard CSS Color Functions |
| 87 | + |
| 88 | +#### `oklab()` and `oklch()` |
| 89 | + |
| 90 | +The `oklab()` (cubic) and `oklch()` (cylindrical) functions provide access to an |
| 91 | +unbounded gamut of colors in a perceptually uniform space. Authors can use these |
| 92 | +functions to define reliably uniform colors. For example, the following colors |
| 93 | +are perceptually similar in lightness and saturation: |
| 94 | + |
| 95 | +```scss |
| 96 | +$pink: oklch(64% 0.196 353); // hsl(329.8 70.29% 58.75%) |
| 97 | +$blue: oklch(64% 0.196 253); // hsl(207.4 99.22% 50.69%) |
| 98 | +``` |
| 99 | + |
| 100 | +The `oklch()` format uses consistent "lightness" and "chroma" values, while the |
| 101 | +`hsl()` format shows dramatic changes in both "lightness" and "saturation". As |
| 102 | +such, `oklch` is often the best space for consistent transforms. |
| 103 | + |
| 104 | +#### `lab()` and `lch()` |
| 105 | + |
| 106 | +The `lab()` and `lch()` functions provide access to an unbounded gamut of colors |
| 107 | +in a space that's less perpetually-uniform but more widely-adopted than OKLab |
| 108 | +and OKLCH. |
| 109 | + |
| 110 | +#### `hwb()` |
| 111 | + |
| 112 | +Sass now supports a top-level `hwb()` function that uses the same syntax as |
| 113 | +CSS's built-in `hwb()` syntax. |
| 114 | + |
| 115 | +#### `color()` |
| 116 | + |
| 117 | +The new `color()` function provides access to a number of specialty spaces. Most |
| 118 | +notably, `display-p3` is a common space for wide-gamut monitors, making it |
| 119 | +likely one of the more popular options for authors who simply want access to a |
| 120 | +wider range of colors. For example, P3 greens are significantly 'brighter' and |
| 121 | +more saturated than the greens available in sRGB: |
| 122 | + |
| 123 | +```scss |
| 124 | +$fallback-green: rgb(0% 100% 0%); |
| 125 | +$brighter-green: color(display-p3 0 1 0); |
| 126 | +``` |
| 127 | + |
| 128 | +Sass will natively support all predefined color spaces declared in the Colors |
| 129 | +Level 4 specification. It will also support unknown color spaces, although these |
| 130 | +can't be converted to and from and other color space. |
| 131 | + |
| 132 | +### New Sass Color Functions |
| 133 | + |
| 134 | +#### `color.channel()` |
| 135 | + |
| 136 | +This function returns the value of a single channel in a color. By default, it |
| 137 | +only supports channels that are available in the color's own space, but you can |
| 138 | +pass the `$space` parameter to return the value of the channel after converting |
| 139 | +to the given space. |
| 140 | + |
| 141 | +```scss |
| 142 | +$brand: hsl(0 100% 25.1%); |
| 143 | + |
| 144 | +// result: 25.1% |
| 145 | +$hsl-lightness: color.channel($brand, "lightness"); |
| 146 | + |
| 147 | +// result: 37.67% |
| 148 | +$oklch-lightness: color.channel($brand, "lightness", $space: oklch); |
| 149 | +``` |
| 150 | + |
| 151 | +#### `color.space()` |
| 152 | + |
| 153 | +This function returns the name of the color's space. |
| 154 | + |
| 155 | +```scss |
| 156 | +// result: hsl |
| 157 | +$hsl-space: color.space(hsl(0 100% 25.1%)); |
| 158 | + |
| 159 | +// result: oklch |
| 160 | +$oklch-space: color.space(oklch(37.7% 38.75% 29.23deg)); |
| 161 | +``` |
| 162 | + |
| 163 | +#### `color.is-in-gamut()`, `color.is-legacy()` |
| 164 | + |
| 165 | +These functions return various facts about the color. `color.is-in-gamut()` |
| 166 | +returns whether the color is in-gamut for its color space (as opposed to having |
| 167 | +one or more of its channels out of bounds, like `rgb(300 0 0)`). |
| 168 | +`color.is-legacy()` returns whether the color is a legacy color in the `rgb`, |
| 169 | +`hsl`, or `hwb` color space. |
| 170 | + |
| 171 | +#### `color.is-powerless()` |
| 172 | + |
| 173 | +This function returns whether a given channel is "powerless" in the given color. |
| 174 | +This is a special state that's defined for individual color spaces, which |
| 175 | +indicates that a channel's value won't affect how a color is displayed. |
| 176 | + |
| 177 | +```scss |
| 178 | +$grey: hsl(0 0% 60%); |
| 179 | + |
| 180 | +// result: true, because saturation is 0 |
| 181 | +$hue-powerless: color.is-powerless($grey, "hue"); |
| 182 | + |
| 183 | +// result: false |
| 184 | +$hue-powerless: color.is-powerless($grey, "lightness"); |
| 185 | +``` |
| 186 | + |
| 187 | +#### `color.same()` |
| 188 | + |
| 189 | +This function returns whether two colors will be displayed the same way, even if |
| 190 | +this requires converting between spaces. This is unlike the `==` operator, which |
| 191 | +always considers colors in different non-legacy spaces to be inequal. |
| 192 | + |
| 193 | +```scss |
| 194 | +$orange-rgb: #ff5f00; |
| 195 | +$orange-oklch: oklch(68.72% 20.966858279% 41.4189852913deg); |
| 196 | + |
| 197 | +// result: false |
| 198 | +$equal: $orange-rgb == $orange-oklch; |
| 199 | + |
| 200 | +// result: true |
| 201 | +$same: color.same($orange-rb, $orange-oklch); |
| 202 | +``` |
| 203 | + |
| 204 | +### Existing Sass Color Functions |
| 205 | + |
| 206 | +#### `color.scale()`, `color.adjust()`, and `color.change()` |
| 207 | + |
| 208 | +By default, all Sass color transformations are handled and returned in the color |
| 209 | +space of the original color parameter. However, all relevant functions now allow |
| 210 | +specifying an explicit color space for transformations. For example, lightness & |
| 211 | +darkness adjustments are most reliable in `oklch`: |
| 212 | + |
| 213 | +```scss |
| 214 | +$brand: hsl(0 100% 25.1%); |
| 215 | + |
| 216 | +// result: hsl(0 100% 43.8%) |
| 217 | +$hsl-lightness: color.scale($brand, $lightness: 25%); |
| 218 | + |
| 219 | +// result: hsl(5.76 56% 45.4%) |
| 220 | +$oklch-lightness: color.scale($brand, $lightness: 25%, $space: oklch); |
| 221 | +``` |
| 222 | + |
| 223 | +Note that the returned color is still emitted in the original color space, even |
| 224 | +when the adjustment is performed in a different space. |
| 225 | + |
| 226 | +#### `color.mix()` |
| 227 | + |
| 228 | +The `color.mix()` function will retain its existing behavior for legacy color |
| 229 | +spaces, but for new color spaces it will match CSS's "color interpolation" |
| 230 | +specification. This is how CSS computes which color to use in between two colors |
| 231 | +in a gradient or an animation. |
| 232 | + |
| 233 | +#### Deprecations |
| 234 | + |
| 235 | +A number of existing functions only make sense for legacy colors, and so are |
| 236 | +being deprecated in favor of color-space-friendly functions like |
| 237 | +`color.channel()` and `color.adjust()`: |
| 238 | + |
| 239 | +* `color.red()` |
| 240 | +* `color.green()` |
| 241 | +* `color.blue()` |
| 242 | +* `color.hue()` |
| 243 | +* `color.saturation()` |
| 244 | +* `color.lightness()` |
| 245 | +* `color.whiteness()` |
| 246 | +* `color.blackness()` |
| 247 | +* `adjust-hue()` |
| 248 | +* `saturate()` |
| 249 | +* `desaturate()` |
| 250 | +* `transparentize()`/`fade-out()` |
| 251 | +* `opacify()`/`fade-in()` |
| 252 | +* `lighten()`/`darken()` |
| 253 | + |
| 254 | +## Let Us Know What You Think! |
| 255 | + |
| 256 | +There's lots more detail to this proposal, and it's not set in stone yet. We |
| 257 | +want your feedback on it! Read it over [on GitHub], and [file an issue] with any |
| 258 | +thoughts or concerns you may have. |
| 259 | + |
| 260 | +[on GitHub]: https://github.com/sass/sass/blob/main/proposal/color-4-new-spaces.md#deprecated-functions |
| 261 | +[file an issue]: https://github.com/sass/sass/issues/new |
0 commit comments