You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Currently, theme palettes are generated from an arbitrary base color by assigning that base as --{color}-500 (regardless of how light or dark it is), and then mixing it with white or black to create a stepped 0-1000 scale. This has drawbacks when attempting to design for accessible contrast.
Bootstrap theme functions and documentation rely on the assumption that certain combinations of tokens can be vaguely expected to give enough contrast. But due to the way the raw palettes are built in the first place, the contrast relationships between those tokens are unpredictable. The numbered stops only give a general sense of light/darkness; they don't indicate what precise luminance to expect, or what contrast ratios that color will actually yield when combined with others.
The docs do advise to double-check whether specific color combinations actually pass contrast standards, but that puts the onus on the user to validate the output of Bootstrap's default themes, through a process of trial-and-error.
Suggested Alternative
Align the numbered palette stops to predetermined relative luminance (Y) values, across the board.
Calibrate this luminance scale on an exponential curve, so that color pairs selected from the numbered range will give predictable contrast ratios.
Since this would mean the original base color no longer sits at --{color}-500, keep that base color separately available for themeing through a non-numbered --{color} token, and add a max-contrast --{color}-con token to go alongside it.
Here is what the luminance scale would look like if calibrated to two different contrast standards; WCAG 2 on the left and APCA on the right.
In both versions, the median stop (500) has about equal contrast against black or white (according to that system). In fact, any color pair separated by a span of 500 gives the same contrast. Other span sizes give predictable contrasts as well. This means if you need to achieve a given contrast ratio, it becomes as simple as selecting two colors that are a certain distance apart on the scale.
The raw palettes would turn out something like this (example calibrated for APCA):
Usage in themes
Suppose you're working with a brand that requires a specific shade of blue. You would use:
--blue for the exact brand blue
--blue-con for max contrast against the brand blue
--blue-xxx for derived shades of blue that offer controlled luminance/contrast.
Upsides
If you need to set a style rule that meets contrast standards (or fix a style rule that has failed a check), this scale would take trial-and-error out of the process.
Certain style rules within Bootstrap ride a line where they may pass in one color palette, but fail when switching to another palette. Standardizing the raw palettes themselves could automatically bump many of those failing cases onto the right side of the line.
Providing the means to choose between WCAG 2 or APCA allows designers to use their own judgment as to which standard is a better fit for their project & region. In fact, it becomes possible to switch all raw palettes from one scale to the other by changing a single variable. (APCA may eventually become the WCAG 3 standard, but in the meantime designers may be compelled to adhere to WCAG 2. Awkward choice between backproofing or futureproofing, so it seems wise to make both options available and easy to toggle between.)
Implementation
Calculating Luminance
The luminance to target for each stop can be determined by the formula Y = (t ^ ((s-b)/(w-b)) - 1)/(t - 1).
$$\huge Y = \frac{t^\left( \frac{s-b}{w-b} \right)- 1}{t - 1}$$
Where: w is the stop value of white (default: 0) b is the stop value of black (default: 1000) s is a target stop value inbetween b and w t is a precalculated constant. t = 21 for WCAG 2, and t = 3.932778164 is the value I've come up with for APCA
Mixing colors to luminance targets
The simplest way to target a specific luminance is to take the base color, and calculate how much black or white to mix it with in the XYZ color space (using a linear color space for this step is important). However, that means losing out on the rich hue/chroma characteristics attainable from mixing in oklch. Therefore, I'd instead recommend a two-step process:
Use the base color to pre-mix a simple range of reference colors in oklch (very much like the way palettes are mixed now)
Refit that range to our luminance scale, using the XYZ space to interpolate between reference colors.
I have a branch going with a working (albeit unpolished) implementation, but I figured I should post the concept before getting fully carried away with a PR.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Currently, theme palettes are generated from an arbitrary base color by assigning that base as
--{color}-500(regardless of how light or dark it is), and then mixing it with white or black to create a stepped 0-1000 scale. This has drawbacks when attempting to design for accessible contrast.Bootstrap theme functions and documentation rely on the assumption that certain combinations of tokens can be vaguely expected to give enough contrast. But due to the way the raw palettes are built in the first place, the contrast relationships between those tokens are unpredictable. The numbered stops only give a general sense of light/darkness; they don't indicate what precise luminance to expect, or what contrast ratios that color will actually yield when combined with others.
The docs do advise to double-check whether specific color combinations actually pass contrast standards, but that puts the onus on the user to validate the output of Bootstrap's default themes, through a process of trial-and-error.
Suggested Alternative
Y) values, across the board.--{color}-500, keep that base color separately available for themeing through a non-numbered--{color}token, and add a max-contrast--{color}-contoken to go alongside it.Here is what the luminance scale would look like if calibrated to two different contrast standards; WCAG 2 on the left and APCA on the right.
In both versions, the median stop (500) has about equal contrast against black or white (according to that system). In fact, any color pair separated by a span of 500 gives the same contrast. Other span sizes give predictable contrasts as well. This means if you need to achieve a given contrast ratio, it becomes as simple as selecting two colors that are a certain distance apart on the scale.
The raw palettes would turn out something like this (example calibrated for APCA):
Usage in themes
Suppose you're working with a brand that requires a specific shade of blue. You would use:
--bluefor the exact brand blue--blue-confor max contrast against the brand blue--blue-xxxfor derived shades of blue that offer controlled luminance/contrast.Upsides
Implementation
Calculating Luminance
The luminance to target for each stop can be determined by the formula
Y = (t ^ ((s-b)/(w-b)) - 1)/(t - 1).Where:
wis the stop value of white (default: 0)bis the stop value of black (default: 1000)sis a target stop value inbetweenbandwtis a precalculated constant.t = 21for WCAG 2, andt = 3.932778164is the value I've come up with for APCAMixing colors to luminance targets
The simplest way to target a specific luminance is to take the base color, and calculate how much black or white to mix it with in the XYZ color space (using a linear color space for this step is important). However, that means losing out on the rich hue/chroma characteristics attainable from mixing in oklch. Therefore, I'd instead recommend a two-step process:
I have a branch going with a working (albeit unpolished) implementation, but I figured I should post the concept before getting fully carried away with a PR.
All reactions