From 3af4162a52b3c992235218a0e37d79fdf9e2fe18 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Fri, 17 May 2024 16:45:21 -0700 Subject: [PATCH] Document new color spaces See #672 --- source/assets/sass/visual-design/_theme.scss | 4 + source/documentation/operators/equality.md | 6 +- source/documentation/values/colors.md | 346 +++++++++++++++++-- 3 files changed, 317 insertions(+), 39 deletions(-) diff --git a/source/assets/sass/visual-design/_theme.scss b/source/assets/sass/visual-design/_theme.scss index 48f893fcc..108ac2b5e 100644 --- a/source/assets/sass/visual-design/_theme.scss +++ b/source/assets/sass/visual-design/_theme.scss @@ -32,6 +32,10 @@ body { color: var(--text, var(--sl-color--pale-sky)); } +.fade { + opacity: 0.7; +} + ::selection { background: var(--sl-color--iron); } diff --git a/source/documentation/operators/equality.md b/source/documentation/operators/equality.md index bbee2c5d7..ac66ba556 100644 --- a/source/documentation/operators/equality.md +++ b/source/documentation/operators/equality.md @@ -23,7 +23,9 @@ different types: their values are equal when their units are converted between one another. * [Strings][] are unusual in that [unquoted][] and [quoted][] strings with the same contents are considered equal. -* [Colors][] are equal if they have the same red, green, blue, and alpha values. +* [Colors] are equal if they're in the same [color space] and have the same + channel values, *or* if they're both in [legacy color spaces] and have the + same RGBA channel values. * [Lists][] are equal if their contents are equal. Comma-separated lists aren't equal to space-separated lists, and bracketed lists aren't equal to unbracketed lists. @@ -40,6 +42,8 @@ different types: [quoted]: /documentation/values/strings#quoted [unquoted]: /documentation/values/strings#unquoted [Colors]: /documentation/values/colors +[color space]: /documentation/values/colors#color-spaces +[legacy color spaces]: /documentation/values/colors#legacy-color-spaces [Lists]: /documentation/values/lists [`true`, `false`]: /documentation/values/booleans [`null`]: /documentation/values/null diff --git a/source/documentation/values/colors.md b/source/documentation/values/colors.md index 0309d1041..227fa2c8c 100644 --- a/source/documentation/values/colors.md +++ b/source/documentation/values/colors.md @@ -1,77 +1,347 @@ --- title: Colors +table_of_contents: true --- -{% compatibility 'dart: "1.14.0"', 'libsass: "3.6.0"', 'ruby: "3.6.0"', 'feature: "Level 4 Syntax"' %} +{% compatibility 'dart: "1.78.0"', 'libsass: false', 'ruby: false', 'feature: "Color Spaces"' %} + LibSass, Ruby Sass, and older versions of Dart Sass don't support color spaces + other than `rgb` and `hsl`. + + As well as to adding support for new color spaces, this release changed some + details of the way colors were handled. In particular, even the legacy `rgb` + and `hsl` color spaces are no longer clamped to their gamuts; it's now + possible to represent `rgb(500 0 0)` or other out-of-bounds values. In + addition, `rgb` colors are no longer rounded to the nearest integer because + the CSS spec now requires implementations to maintain precision wherever + possible. +{% endcompatibility %} + +{% compatibility 'dart: "1.14.0"', 'libsass: false', 'ruby: "3.6.0"', 'feature: "Level 4 Syntax"' %} LibSass and older versions of Dart or Ruby Sass don't support [hex colors with an alpha channel][]. [hex colors with an alpha channel]: https://drafts.csswg.org/css-color/#hex-notation {% endcompatibility %} -Sass has built-in support for color values. Just like CSS colors, they represent -points in the [sRGB color space][], although many Sass [color functions][] -operate using [HSL coordinates][] (which are just another way of expressing sRGB -colors). Sass colors can be written as hex codes (`#f2ece4` or `#b37399aa`), -[CSS color names][] (`midnightblue`, `transparent`), or the functions -[`rgb()`][], [`rgba()`][], [`hsl()`][], and [`hsla()`][]. +Sass has built-in support for color values. Just like CSS colors, each color +represents a point in a particular color space such as `rgb` or `lab`. Sass +colors can be written as hex codes (`#f2ece4` or `#b37399aa`), [CSS color names] +(`midnightblue`, `transparent`), or color functions like [`rgb()`], [`lab()`], +or [`color()`]. [sRGB color space]: https://en.wikipedia.org/wiki/SRGB [color functions]: /documentation/modules/color -[HSL coordinates]: https://en.wikipedia.org/wiki/HSL_and_HSV [CSS color names]: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#Color_keywords [`rgb()`]: /documentation/modules#rgb -[`rgba()`]: /documentation/modules#rgba -[`hsl()`]: /documentation/modules#hsl -[`hsla()`]: /documentation/modules#hsla +[`lab()`]: /documentation/modules#lab +[`color()`]: /documentation/modules#color {% codeExample 'colors', false %} @debug #f2ece4; // #f2ece4 @debug #b37399aa; // rgba(179, 115, 153, 67%) @debug midnightblue; // #191970 - @debug rgb(204, 102, 153); // #c69 - @debug rgba(107, 113, 127, 0.8); // rgba(107, 113, 127, 0.8) - @debug hsl(228, 7%, 86%); // #dadbdf - @debug hsla(20, 20%, 85%, 0.7); // rgb(225, 215, 210, 0.7) + @debug rgb(204 102 153); // #c69 + @debug lab(32.4% 38.4 -47.7 / 0.7); // lab(32.4% 38.4 -47.7 / 0.7) + @debug color(display-p3 0.597 0.732 0.576); // color(display-p3 0.597 0.732 0.576) === @debug #f2ece4 // #f2ece4 @debug #b37399aa // rgba(179, 115, 153, 67%) @debug midnightblue // #191970 - @debug rgb(204, 102, 153) // #c69 - @debug rgba(107, 113, 127, 0.8) // rgba(107, 113, 127, 0.8) - @debug hsl(228, 7%, 86%) // #dadbdf - @debug hsla(20, 20%, 85%, 0.7) // rgb(225, 215, 210, 0.7) + @debug rgb(204 102 153) // #c69 + @debug lab(32.4% 38.4 -47.7 / 0.7) // lab(32.4% 38.4 -47.7 / 0.7) + @debug color(display-p3 0.597 0.732 0.576) // color(display-p3 0.597 0.732 0.576) {% endcodeExample %} -{% funFact %} - No matter how a Sass color is originally written, it can be used with both - HSL-based and RGB-based functions! -{% endfunFact %} +## Color Spaces + +Sass supports the same set of color spaces as CSS. A Sass color will always be +emitted in the same color space it was written in unless it's in a [legacy color +space] or you convert it to another space using [`color.to-space()`]. All the +other color functions in Sass will always return a color in the same spaces as +the original color, even if the function made changes to that color in another +space. + +[legacy color space]: #legacy-color-spaces +[`color.to-space()`]: /documentation/modules/color#to-space + +Although each color space has bounds on the gamut it expects for its channels, +Sass can represent out-of-gamut values for any color space. This allows a color +from a wide-gamut space to be safely converted into and back out of a +narrow-gamut space without losing information. + +{% headsUp %} + CSS requires that some color functions clip their input channels. For example, + `rgb(500 0 0)` clips its red channel to be within [0, 255] and so is + equivalent to `rgb(255 0 0)` even though `rgb(500 0 0)` is a distinct value + that Sass can represent. You can always use Sass's [`color.change()`] function + to set an out-of-gamut value for any space. + + [`color.change()`]: /documentation/modules/color#change +{% endheadsUp %} + +Following is a full list of all the color spaces Sass supports. You can read +learn about these spaces [on MDN]. + +[on MDN]: https://developer.mozilla.org/en-US/docs/Glossary/Color_space + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SpaceSyntaxChannels [min, max]
rgb* + rgb(102 51 153)
+ #663399
+ rebeccapurple +
+ red [0, 255]; + green [0, 255]; + blue [0, 255] +
hsl*hsl(270 50% 40%) + hue [0, 360]; + saturation [0%, 100%]; + lightness [0%, 100%] +
hwb*hwb(270 20% 40%) + hue [0, 360]; + whiteness [0%, 100%]; + blackness [0%, 100%] +
srgbcolor(srgb 0.4 0.2 0.6) + red [0, 1]; + green [0, 1]; + blue [0, 1] +
srgb-linearcolor(srgb-linear 0.133 0.033 0.319) + red [0, 1]; + green [0, 1]; + blue [0, 1] +
display-p3color(display-p3 0.374 0.21 0.579) + red [0, 1]; + green [0, 1]; + blue [0, 1] +
a98-rgbcolor(a98-rgb 0.358 0.212 0.584) + red [0, 1]; + green [0, 1]; + blue [0, 1] +
prophoto-rgbcolor(prophoto-rgb 0.316 0.191 0.495) + red [0, 1]; + green [0, 1]; + blue [0, 1] +
rec2020color(rec2020 0.305 0.168 0.531) + red [0, 1]; + green [0, 1]; + blue [0, 1] +
xyz, xyz-d65 + color(xyz 0.124 0.075 0.309)
+ color(xyz-d65 0.124 0.075 0.309) +
+ x [0, 1]; + y [0, 1]; + z [0, 1] +
xyz-d50color(xyz-d50 0.116 0.073 0.233) + x [0, 1]; + y [0, 1]; + z [0, 1] +
lablab(32.4% 38.4 -47.7) + lightness [0%, 100%]; + a [-125, 125]; + b [-125, 125] +
lchlch(32.4% 61.2 308.9deg) + lightness [0%, 100%]; + chroma [0, 150]; + hue [0deg, 360deg] +
oklaboklab(44% 0.088 -0.134) + lightness [0%, 100%]; + a [-0.4, 0.4]; + b [-0.4, 0.4] +
oklchoklch(44% 0.16 303.4deg) + lightness [0%, 100%]; + chroma [0, 0.4]; + hue [0deg, 360deg] +
+ +Spaces marked with * are [legacy color spaces]. + +[legacy color spaces]: #legacy-color-spaces + +## Missing Channels + +Colors in CSS and Sass can have "missing channels", which are written `none` and +represent a channel whose value isn't known or doesn't affect the way the color +is rendered. For example, you might write `hsl(none 0% 50%)`, because the hue +doesn't matter if the saturation is `0%`. In most cases, missing channels are +just treated as 0 values, but they do come up occasionally: + +* If you're mixing colors together, either as part of CSS interpolation for + something like an animation or using Sass's [`color.mix()`] function, missing + channels always take on the other color's value for that channel if possible. + + [`color.mix()`]: /documentation/modules/color#mix + +* If you convert a color with a missing channel to another space that has an + analogous channel, that channel will be set to `none` after the conversion is + complete. + +Although [`color.channel()`] will return 0 for missing channels, you can always +check for them using [`color.is-missing()`]. -CSS supports many different formats that can all represent the same color: its -name, its hex code, and [functional notation][]. Which format Sass chooses to -compile a color to depends on the color itself, how it was written in the -original stylesheet, and the current output mode. Because it can vary so much, -stylesheet authors shouldn't rely on any particular output format for colors -they write. +[`color.channel()`]: /documentation/modules/color#channel +[`color.is-missing()`]: /documentation/modules/color#is-missing -[functional notation]: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value +{% codeExample 'missing-channels', false %} + @use 'sass:color'; -Sass supports many useful [color functions][] that can be used to create new -colors based on existing ones by [mixing colors together][] or [scaling their -hue, saturation, or lightness][]. + $grey: hsl(none 0% 50%); + + @debug color.mix($grey, blue, $method: hsl); // hsl(240, 50%, 50%) + @debug color.to-space($grey, lch); // lch(53.3889647411% 0 none) + === + @use 'sass:color' + + $grey: hsl(none 0% 50%) + + @debug color.mix($grey, blue, $method: hsl) // hsl(240, 50%, 50%) + @debug color.to-space($grey, lch) // lch(53.3889647411% 0 none) +{% endcodeExample %} + +### Powerless Channels + +A color channel is considered "powerless" under certain circumstances its value +doesn't affect the way the color is rendered on screen. The CSS spec requires +that when a color is converted to a new space, any powerless channels are +replaced by `none`. Sass does this in all cases except conversions to legacy +spaces, to guarantee that converting to a legacy space always produces a color +that's compatible with older browsers. + +For more details on powerless channels, see [`color.is-powerless()`]. + +[`color.is-powerless()`]: /documentation/modules/color#is-powerless + +## Legacy Color Spaces + +Historically, CSS and Sass only supported the standard RGB gamut, and only +supported the `rgb`, `hsl`, and `hwb` functions for defining colors. Because at +the time all colors used the same gamut, every color function worked with every +color regardless of its color space. Sass still preserves this behavior, but +only for older functions and only for colors in these three "legacy" color +spaces. Even so, it's still a good practice to explicitly specify the `$space` +you want to work in when using color functions. + +Sass will also freely convert between different legacy color spaces when +converting legacy color values to CSS. This is always safe, because they all use +the same underlying color model, and this helps ensure that Sass emits colors in +as compatible a format as possible. + +## Color Functions + +Sass supports many useful [color functions] that can be used to create new +colors based on existing ones by [mixing colors together] or [scaling their +channel values]. When calling color functions, color spaces should always be +written as unquoted strings to match CSS, while channel names should be written +as quoted strings so that channels like `"red"` aren't parsed as color values. [mixing colors together]: /documentation/modules/color#mix -[scaling their hue, saturation, or lightness]: /documentation/modules/color#scale +[scaling their channel values]: /documentation/modules/color#scale + +{% funFact %} + Sass color functions can automatically convert colors between spaces, which + makes it easy to do transformations in perceptually-uniform color spaces like + Oklch. But they'll *always* return a color in the same space you gave it, + unless you explicitly call [`color.to-space()`] to convert it. + + [`color.to-space()`]: /documentation/modules/color#to-space +{% endfunFact %} {% codeExample 'color-formats', false %} + @use 'sass:color'; + $venus: #998099; - @debug scale-color($venus, $lightness: +15%); // #a893a8 - @debug mix($venus, midnightblue); // #594d85 + @debug color.scale($venus, $lightness: +15%, $space: oklch); + // rgb(170.1523703626, 144.612080603, 170.1172627174) + @debug color.mix($venus, midnightblue, $method: oklch); + // rgb(95.9363315581, 74.5687109346, 133.2082569526) === + @use 'sass:color' + $venus: #998099 - @debug scale-color($venus, $lightness: +15%) // #a893a8 - @debug mix($venus, midnightblue) // #594d85 + @debug color.scale($venus, $lightness: +15%, $space: oklch) + // rgb(170.1523703626, 144.612080603, 170.1172627174) + @debug color.mix($venus, midnightblue, $method: oklch) + // rgb(95.9363315581, 74.5687109346, 133.2082569526) {% endcodeExample %}