hsluv-sass
hsluv-sass copied to clipboard
New API
HSLuv have a great potential in a design systems, where designer may define base colors or even a set of hue values and the system will take care of rest. To achieve this, we should provide a set of tools for native HSLuv color manipulation.
The first commit contains mocks for future API which I want to discuss (second hardest thing in a computer science). In general it looks like:
@use "hsluv/tools" as hsluv;
@import "hsluv/hsluv";
button {
$baseColor: hsluv.color(10deg, 80%, 60%);
$backgroundColor: hsluv.contrast-darken($baseColor, 'AA');
$borderColor: hsluv.rotate($backgroundColor, 10deg);
color: hsluv($baseColor);
background-color: hsluv($backgroundColor);
border: 1px solid hsluv($borderColor);
}
I still not sure if it's a good idea to have @use
and @import
in one file, maybe we may consider to add hsluv.to-hsluv()
and hsluv.to-hpluv()
functions to be able to just @use
. Naming of hsluv.color()
can be confusing as for me, an alternative is something like hsluv.create()
Also, color contrast functions accepts both numeric ratio and AA
and AAA
values which will contain a small reduction to satisfy automated color contrast checkers (some colors after conversion to RGB may produce a little bit lower contrast ratio)
@boronine @apexskier what do you think?
Hi @Ptico sorry for the delay.
I just looked at the Sass color module here: https://sass-lang.com/documentation/modules/color
I'm not really impressed with their API, there's too many functions that are poorly defined. Some things just don't make sense, what happens when you adjust lightness to be above 100% or below 0%? Presumably it clips.. The API suggests using scale
to adjust by percentages, but this seems even worse, for example if you scale lightness by 50% for two different colors you might have a huge change in one color and a tiny change in another color, so what is 50% supposed to mean intuitively? At this point it seems easier to just use a color picker.
I think we should make a smaller, lower level API that gives users an opportunity to do EXACTLY what they want, relying on the sass:math
module. Once we have that we can consider adding high-level helper functions.
At minimum, it seems that we need functions to extract HSLuv values from an existing color, e.g. their official brand color:
-
get-hue(#abc123) -> deg
-
get-saturation(#abc123) -> %
-
get-lightness(#abc123) -> %
-
get-hpluv-saturation(#abc123) -> %
(this only works with a subset of colors, we can probably use@error
to deal with this)
(Or perhaps just implement get-hsluv
and get-hpluv
functions instead?)
The user can then use sass:math
to manipulate these values and plug them back into the hsluv contructor to get new colors.
For contrast, the minimum implementation would be something like this:
-
get-contrast-ratio($lightness1 %, $lightness2 %)
-
get-darker($lightness %, $ratio) -> %
(throws error when out of bounds) -
get-lighter($lightness %, $ratio) -> %
(throws error when out of bounds)
Along with these functions we can provide documentation with some minimum contrast ratios defined by some standards (AA, AAA etc.). I don't see much value in adding these to the API since these are only minimum values, not exact targets. Or maybe provide them as constants?
I don't think we should expose an internal representation of HSLuv to the public API unless we really have to. If we can get away with using native Sass colors everywhere it would be simpler for the user.
What are your thoughts? There's a lot going on in this discussion, so I'd be happy to focus on the minimal merge-able implementation here and delay some stuff (e.g. contrast) until the next PR.
I agree with minimizing an API, though it requires some work on documentation to show users an examples of how to work with sass:math and hsluv
For the rest of new API, the main idea is to work with hsluv as a first class sitizen. It will be useful for designers who prefer to use HSLuv as a main color space, as well as for principal frontend engineers who will build a design systems on top of it. An internal representation is nothing more than (h:, s:, l:) map. The problem with a SASS color is that we have to round an RGB values to the nearest 0-255 whole number and each conversion from/to RGB loses the color information, so if we define a base color, then do a lot of transformations through the styles, we may have pretty unexpected results.
Also, manipulating an rgb colors with this library makes no sense for me, it's just duplicating what we already have with sass:color and almost doesn't add any value. However, we can always translate an existing sass color with hsluv.create(#123abc)
I'm very out of date on SASS syntax, but I have a question about your example:
Why do you need the hsluv()
"calls" when using the color variables? I'd expect:
button {
$baseColor: hsluv(10deg, 80%, 60%);
$backgroundColor: hsluv.contrast-darken($baseColor, 'AA');
$borderColor: hsluv.rotate($backgroundColor, 10deg);
color: $baseColor;
background-color: $backgroundColor;
border: 1px solid $borderColor;
}
As is, I see two problems:
- I think people would commonly forget to convert back to a sass color (creating broken css)
- The utilities can't be used with other color representations (because they expect the internal
('h', 's', 'l')
representation) (hsluv.contrast-darken(#ff0000, 'AA');
won't work)
I'd prefer if the hsluv.color
/hsluv.create
function and ('h', 's', 'l')
representation was internal/private to the hsluv
implementation. (This assumes it can translate a value of any sass color representation, as implied in @Ptico's last comment.)
e.g.
@function get-hue($color) {
$raw-hsluv: create($color);
@return map-get($raw-hsluv, 'h');
}
// usage: hsluv.get-hue(#ff0000)
@function contrast($color, $op: 'darken', $ratio: 'AA') {
$raw-hsluv: create($color);
$h: map-get($raw-hsluv, 'h');
$s: map-get($raw-hsluv, 's');
$l: map-get($raw-hsluv, 'l');
// ...
@return hsluv($raw-hsluv)
}
I agree with much being said, but I'm a little overwhelmed by how many issues are discussed simultaneously. I'm going to try to summarize our discussion here. Seems that we have 3 proposals (ignoring function names for now):
A. Non-magic minimal version, 6 functions added
$brandColor: #abc123;
$brandH: hsluv.get-hue($brandColor);
$brandS: hsluv.get-saturation($brandColor);
$brandL: hsluv.get-lightness($brandColor);
$compH: ($brandH + 180deg) % 360;
.banner {
color: hsluv($brandH, $brandS, $brandL);
background-color: hsluv($compH, $brandS, hsluv.get-darker($brandL, hsluv.AA));
border: 1px solid hsluv($compH, $brandS, hsluv.get-darker($brandL, hsluv.AA * 1.5));
}
B. Magic version exporting HSLuv internal representation, ~17 functions added
$brandColor: hsluv.create(#abc123);
$compColor: hsluv.rotate($brandColor, 180deg);
.banner {
color: hsluv.hex($brandColor);
background-color: hsluv.hex(hsluv.contrast-darken($compColor, 'AA'));
border: 1px solid hsluv.hex(hsluv.contrast-darken($compColor, 5.5));
}
C. Magic version hiding HSLuv internal representation, ~16 functions added
$brandColor: #abc123;
$compColor: hsluv.rotate($brandColor, 180deg);
.banner {
color: $brandColor;
background-color: hsluv.contrast-darken($compColor, 'AA');
border: 1px solid hsluv.contrast-darken($compColor, 5.5);
}
My preference is option A, then option C, then option B. I agree with @apexskier's arguments to choose option C over option B. @Ptico raises an issue with option C regarding rounding, but can we not avoid rounding if we use CSS rgb(...)
constructor rather than hex?
Hi @boronine @apexskier My main point actually is to operate HSLuv kind of natively, because I don’t see any value added by this library in operating rgb colors
Would you mind to have a quick text or voice chat somewhere like discord or gitter to better explain an idea?
@Ptico I'd prefer async communication. Can you explain your idea here? I understood your proposal to be option B above. Are you proposing something different? If so can you draft a list of functions you're proposing?
I feels like I have a small language barrier to properly explain an idea, but i'll try:
Let's say, we have a team of designer and frontend developer who wants to create a design system for an upcoming web-app from scratch. Buttons, modals, typography, etc.
The designer have to choose one or two main (brand) colors, then also some special, like for errors, dangerous actions, accents, etc. Every color will have a lot of deviations in terms of lightness and saturation and they might be able to quickly add an additional ones. An RGB always was a bad choice because for the majority of people three numbers in a range from 0 to 255 means nothing unless pasted to color picker. It's hard to mix three colors in your head. It's also hard to say what is the difference between two colors in terms of lightness and saturation.
And here is where HSL-kind of colors shines. I know that the range between 120-140deg is a green(ish) color, 80% is pretty saturated and 50% is bright enough (you know this better than me). So even if we already have some brand colors defined in RGB it's simplier to just convert them to HSLuv once and then communicate with this format instead of rgb.
Then we might create some UI patterns, lets say button. We have picked a main color for button, then we might want to have some accent in a form of darker border and to make it great-looking we want a text to be not just white, but bright shade of main color with an accessible contrast. We also want to describe CSS rules just once instead of duplicating the code for each color but make them looks uniform in terms of visual brightness. Here we come to HSLuv
From the designer's perspective the process might look like this: Designer installs some HSLuv color picker to his favourite design tool, converts existings colors to HSLuv and draws the UI elements. To create a nice button border he just move the L slider to make it darker, and to draw the danger button he just Cmd+C/Cmd+V the main one and move the H slider somewhere to 10deg. Easy, handy, happy days!
Then, the frontend developer should implement this design in SCSS. The process might looks similar to the designer's one (my suggestion) or the designer have to convert all the base colors back to the RGB (where 10deg, 80%, 50% becomes #de3442) gives them to the frontent developer forcing him to continously extract HSLuv values from RGB and likely converting them back and forth loosing the precision and making designer unhappy (and no, using the rgb() constructor will not help, because it's just slightly different representation of the same values).
So, my point is to give the frontend developers a tool for a native HSLuv flow where designers and developers communicate the same language and perform the same operations. Also, in an HSLuv native flow, internal representation is easier to debug because the developer can see the same values as in designs
I could try to create a real world example and test different approaches there
Designer installs some HSLuv color picker to his favourite design tool, converts existings colors to HSLuv and draws the UI elements. To create a nice button border he just move the L slider to make it darker, and to draw the danger button he just Cmd+C/Cmd+V the main one and move the H slider somewhere to 10deg. Easy, handy, happy days!
Ok I can imagine a tool like that.
Then, the frontend developer should implement this design in SCSS. The process might looks similar to the designer's one (my suggestion) or the designer have to convert all the base colors back to the RGB (where 10deg, 80%, 50% becomes #de3442) gives them to the frontent developer forcing him to continously extract HSLuv values from RGB and likely converting them back and forth loosing the precision and making designer unhappy
I don't understand this part. The designer can provide HSLuv values and the frontend developer can use hsluv-sass to plug them into SCSS, no? Why would the frontend developer need to convert colors back and forth?
my point is to give the frontend developers a tool for a native HSLuv flow where designers and developers communicate the same language and perform the same operations
The current solution to this problem is for designers to use SASS + hsluv-sass and then pass the .scss file to the frontend developer.
Are you suggesting making some kind of app? I'm not opposed to it though I probably won't have time to contribute. If you want to take a shot at it, I'd happily put it under the https://github.com/hsluv org and link to it from the homepage. If you want to discuss it further perhaps the best place is to open an issue under the main repo: https://github.com/hsluv/hsluv/issues
For this comment thread let's go back to the API question. My summary of the current proposals in this comment above: https://github.com/hsluv/hsluv-sass/pull/9#issuecomment-854775270
Please let me know which option you choose or if you have another option, please make a list of functions or a code sample to demonstrate it.
And here is where HSL-kind of colors shines. I know that the range between 120-140deg is a green(ish) color, 80% is pretty saturated and 50% is bright enough (you know this better than me).
Definitely - this is the clear value of HSLuv and the main point of the project.
Then, the frontend developer should implement this design in SCSS. The process might looks similar to the designer's one (my suggestion) or the designer have to convert all the base colors back to the RGB (where 10deg, 80%, 50% becomes #de3442) gives them to the frontent developer forcing him to continously extract HSLuv values from RGB and likely converting them back and forth loosing the precision and making designer unhappy (and no, using the rgb() constructor will not help, because it's just slightly different representation of the same values).
forcing him to continously extract HSLuv values from RGB and likely converting them back and forth
This point doesn't make sense to me, as I think @boronine also mentioned.
If the developer is able to deal with the built-in sass color values, they should never need to extract HSLuv or RGB values. The only place that needs to happen is inside the hsluv module. My concern with the original proposal in the thread is that you are extracting something. It's not as bad as extracting individual h
, s
, and l
number
values, but it is extracting the (h, s, l)
map value when you could use color
values directly.
loosing the precision
I agree, the loss of precision is the main downside to my suggestion, but I personally don't think it's going to be a huge deal. Designing for the web already means designers already need to be aware of many different factors that cause loss of color fidelity - OS/browser handling, different monitors, etc. I think the value of having a color be a color, and usable in a property directly, is high.
Let's say, we have a team of designer and frontend developer who wants to create a design system for an upcoming web-app from scratch. Buttons, modals, typography, etc.
I like this example, but I think we should also consider non-greenfield projects, which are very common.
Say a team embedded in a large company wants to start using HSLuv, but needs to embed within an existing SASS library where they have a large set of colors already defined as hex colors (e.g. $primary: #212e61
. It'd be nice for them to do something like $primary-shadow: hsluv.darken($primary, 0.2)
instead of $primary-shadow(hsluv.create($primary), 0.2)
.
For both cases, I think introducing a new datatype is going to cause unnecessary boilerplate and verboseness as a codebase grows due to the need to convert when declaring, using, and potentially modifying existing colors.
By the tool I mean hsluv-sass itself
I don't understand this part. The designer can provide HSLuv values and the frontend developer can use hsluv-sass to plug them into SCSS, no? Why would the frontend developer need to convert colors back and forth?
Lets take a look at our examples in a context of HSLuv native flow:
The designer gives us a "danger" color: 10deg, 80%, 50%
Option A:
$dangerH: 10deg;
$dangerS: 80%;
$dangerL: 50%;
$dangerTextL: hsluv.contrast-lighten($dangerL, hsluv.AA);
$dangerBorderL: $dangerL - 20;
.dangerButton {
background-color: hsluv($dangerH, $dangerS, $dangerL);
color: hsluv($dangerH, $dangerS, $dangerTextL);
border-color: hsluv($dangerH, $dangerS, $dangerBorderL);
}
No need for RGB conversions of course, but imagine having 10 different HSLuv colors.
Option C:
$dangerColor: #de3442;
.dangerButton {
background-color: $dangerColor;
color: hsluv.contrast-lighten($dangerColor, hsluv.AA);
border-color: hsluv.darken($dangerColor, 0.2);
Here we:
a) Forced to operate with SASS colors, so the designer would have to convert everything to RGB b) The only value hsluv-sass adds here is a color-contrast functions, because other operations may be done with sass:color and besides accessability there is no need for HSLuv at all
Option B
$dangerColor: hsluv.create(10deg, 80%, 50%);
.dangerButton {
background-color: hsluv($dangerColor);
color: hsluv(hsluv.contrast-lighten($dangerColor, hsluv.AA));
border-color: hsluv(hsluv.darken($dangerColor, 20%));
}
There is a downside of requirement to apply hsluv()
every time we need to assign an HSLuv color to actual CSS, but for a much larger codebases IMO it's much easier to operate with HSLuv colors instead of separate HSLuv values or RGB colors
For option C, this is what I'm suggesting:
$dangerColor: hsluv.create(10deg, 80%, 50%);
.dangerButton {
background-color: $dangerColor;
color: hsluv.contrast-lighten($dangerColor, hsluv.AA);
border-color: hsluv.darken($dangerColor, 0.2);
}
No need to convert to RBG.
b) The only value hsluv-sass adds here is a color-contrast functions, because other operations may be done with sass:color and besides accessability there is no need for HSLuv at all
HSLuv adds additional value in that its functions operate on a different geometry than sass:color. I also think interoperability with sass colors is a big benefit.
I like this example, but I think we should also consider non-greenfield projects, which are very common
Non-greenfield projects are common, the partial usage of HSLuv in them is not, but anyway we already have an API for this kind of usage.
Think of new (optional) API as of tool for the new projects which are going to use HSLuv in all stages of a design pipeline and would not mix SASS colors with HSLuv.
No need to convert to RBG.
We will need to implement native API internally anyway, would it work if we will let users choose which one they will use?
Like:
@use 'hsluv/native'; // For native operations
//or
@use 'hsluv/tools'; // For SASS color operations
It will not require too much effort because hsluv/native
will be a backend for hsluv-tools
I like that idea
I think the value offered by saving a few keystrokes will be destroyed by wasting time reading though this bloated API, trying to understand what "native" means, trying to understand what happens when you go out of bounds, or getting compilation errors and giving up on hsluv-sass completely. Instead of giving the user 30 different wrappers for hsluv
function, it would be better to just explain to the user the meaning of H, S, and L. After they understand it, they never have to reference the documentation again.
I did some tests trying to use rgb
and rgba
to avoid rounding, but Sass always rounds these to integers so these formats are indeed equivalent to hex. However the difference between two off-by-one hex colors is imperceptible, so I believe rounding is a non-issue in practice. Unless we can come up with a practical scenario where rounding is problem, all "native" API options should be rejected, leaving us with minimal API (option A) and sass:color clone (option C).
Ok, here is another suggestion: let’s have a minimal set of tools, like color contrast and component extraction and collection of helpers in an external, third-party package. Would it be fair to mention it in a readme?
@Ptico before we consider making another repo, why don't we take stock of the discussion.
You mention 3 things:
- color contrast
- component extraction (rgb -> hsluv)
- helper functions (sass:color clone)
I think all of us in this discussion agree that 1 and 2 would add value to this repo. Am I right? I propose we implement those first, release them as v2.1.0, then open another issue for helper functions, summarizing the relevant discussion from this thread. Let's make incremental progress.
At the end of the day I think @apexskier should make the call as the original creator of this port. I will merely provide my opinions.
@boronine yeah, that’s what I meant. Also, I’m thinking about to update my company’s design system and then make another proposal based on a code extraction from the real usecase
@Ptico ah okay, I misread your comment. Great that we agree!
I’m thinking about to update my company’s design system and then make another proposal based on a code extraction from the real usecase
This sounds like a great idea!
Have there been any updates on this pull request and/or discussion? I am very interested in having more SASS hsluv methods to work with in generating color schemes for my projects, particularly the ability to create an hsluv color object and HSL properties from a HEX color. Cool project!
@badfeather I think RGB/hex -> HSLuv conversion can be valuable to expose. When generating palettes from scratch there is not much need for it, but when working around a color specified elsewhere, e.g. a brand color, it may be useful. I would make a separate issue for it.
@boronine Yes, exactly. It would be very useful to take a brand's colors and convert them to HSLuv colors in order to generate shades. I will create a separate issue.