Skip to content

Commit

Permalink
Document new color spaces
Browse files Browse the repository at this point in the history
See #672
  • Loading branch information
nex3 committed May 17, 2024
1 parent 3a50fc0 commit f208023
Show file tree
Hide file tree
Showing 2 changed files with 296 additions and 38 deletions.
4 changes: 4 additions & 0 deletions source/assets/sass/visual-design/_theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ body {
color: var(--text, var(--sl-color--pale-sky));
}

.fade {
opacity: 0.7;
}

::selection {
background: var(--sl-color--iron);
}
Expand Down
330 changes: 292 additions & 38 deletions source/documentation/values/colors.md
Original file line number Diff line number Diff line change
@@ -1,77 +1,331 @@
---
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`.
{% 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

<table class="sl-c-table">
<tr>
<th scope="col">Space</th>
<th scope="col">Syntax</th>
<th scope="col">Channels [min, max]</th>
</tr>
<tr>
<th scope="row"><code>rgb</code>*</th>
<td>
<code>rgb(102 51 153)</code><br>
<code>#663399</code><br>
<code>rebeccapurple</code>
</td>
<td>
red <span class="fade">[0, 255]</span>;
green <span class="fade">[0, 255]</span>;
blue <span class="fade">[0, 255]</span>
</td>
</tr>
<tr>
<th scope="row"><code>hsl</code>*</th>
<td><code>hsl(270 50% 40%)</code></td>
<td>
hue <span class="fade">[0, 360]</span>;
saturation <span class="fade">[0%, 100%]</span>;
lightness <span class="fade">[0%, 100%]</span>
</td>
</tr>
<tr>
<th scope="row"><code>hwb</code>*</th>
<td><code>hwb(270 20% 40%)</code></td>
<td>
hue <span class="fade">[0, 360]</span>;
whiteness <span class="fade">[0%, 100%]</span>;
blackness <span class="fade">[0%, 100%]</span>
</td>
</tr>
<tr>
<th scope="row"><code>srgb</code></th>
<td><code>color(srgb 0.4 0.2 0.6)</code></td>
<td>
red <span class="fade">[0, 1]</span>;
green <span class="fade">[0, 1]</span>;
blue <span class="fade">[0, 1]</span>
</td>
</tr>
<tr>
<th scope="row"><code>srgb-linear</code></th>
<td><code>color(srgb-linear 0.133 0.033 0.319)</code></td>
<td>
red <span class="fade">[0, 1]</span>;
green <span class="fade">[0, 1]</span>;
blue <span class="fade">[0, 1]</span>
</td>
</tr>
<tr>
<th scope="row"><code>display-p3</code></th>
<td><code>color(display-p3 0.374 0.21 0.579)</code></td>
<td>
red <span class="fade">[0, 1]</span>;
green <span class="fade">[0, 1]</span>;
blue <span class="fade">[0, 1]</span>
</td>
</tr>
<tr>
<th scope="row"><code>a98-rgb</code></th>
<td><code>color(a98-rgb 0.358 0.212 0.584)</code></td>
<td>
red <span class="fade">[0, 1]</span>;
green <span class="fade">[0, 1]</span>;
blue <span class="fade">[0, 1]</span>
</td>
</tr>
<tr>
<th scope="row"><code>prophoto-rgb</code></th>
<td><code>color(prophoto-rgb 0.316 0.191 0.495)</code></td>
<td>
red <span class="fade">[0, 1]</span>;
green <span class="fade">[0, 1]</span>;
blue <span class="fade">[0, 1]</span>
</td>
</tr>
<tr>
<th scope="row"><code>rec2020</code></th>
<td><code>color(rec2020 0.305 0.168 0.531)</code></td>
<td>
red <span class="fade">[0, 1]</span>;
green <span class="fade">[0, 1]</span>;
blue <span class="fade">[0, 1]</span>
</td>
</tr>
<tr>
<th scope="row"><code>xyz</code>, <code>xyz-d65</code></th>
<td>
<code>color(xyz 0.124 0.075 0.309)</code><br>
<code>color(xyz-d65 0.124 0.075 0.309)</code>
</td>
<td>
x <span class="fade">[0, 1]</span>;
y <span class="fade">[0, 1]</span>;
z <span class="fade">[0, 1]</span>
</td>
</tr>
<tr>
<th scope="row"><code>xyz-d50</code></th>
<td><code>color(xyz-d50 0.116 0.073 0.233)</code></td>
<td>
x <span class="fade">[0, 1]</span>;
y <span class="fade">[0, 1]</span>;
z <span class="fade">[0, 1]</span>
</td>
</tr>
<tr>
<th scope="row"><code>lab</code></th>
<td><code>lab(32.4% 38.4 -47.7)</code></td>
<td>
lightness <span class="fade">[0%, 100%]</span>;
a <span class="fade">[-125, 125]</span>;
b <span class="fade">[-125, 125]</span>
</td>
</tr>
<tr>
<th scope="row"><code>lch</code></th>
<td><code>lch(32.4% 61.2 308.9deg)</code></td>
<td>
lightness <span class="fade">[0%, 100%]</span>;
chroma <span class="fade">[0, 150]</span>;
hue <span class="fade">[0deg, 360deg]</span>
</td>
</tr>
<tr>
<th scope="row"><code>oklab</code></th>
<td><code>oklab(44% 0.088 -0.134)</code></td>
<td>
lightness <span class="fade">[0%, 100%]</span>;
a <span class="fade">[-0.4, 0.4]</span>;
b <span class="fade">[-0.4, 0.4]</span>
</td>
</tr>
<tr>
<th scope="row"><code>oklch</code></th>
<td><code>oklch(44% 0.16 303.4deg)</code></td>
<td>
lightness <span class="fade">[0%, 100%]</span>;
chroma <span class="fade">[0, 0.4]</span>;
hue <span class="fade">[0deg, 360deg]</span>
</td>
</tr>
</table>

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.

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.
{% codeExample 'missing-channels', false %}
@use 'sass:color';

[functional notation]: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
$grey: hsl(none 0% 50%);

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][].
@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].

[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 %}

0 comments on commit f208023

Please sign in to comment.