1 | # `@adobe/leonardo-contrast-colors`
|
2 |
|
3 | [![npm version](https://badge.fury.io/js/%40adobe%2Fleonardo-contrast-colors.svg)](https://www.npmjs.com/package/@adobe/leonardo-contrast-colors)
|
4 | [![Package size](https://badgen.net/packagephobia/publish/@adobe/leonardo-contrast-colors)](https://packagephobia.com/result?p=%40adobe%2Fleonardo-contrast-colors)
|
5 | [![Minified size](https://badgen.net/bundlephobia/min/@adobe/leonardo-contrast-colors)](https://bundlephobia.com/package/@adobe/leonardo-contrast-colors)
|
6 | [![Minified and gzipped size](https://badgen.net/bundlephobia/minzip/@adobe/leonardo-contrast-colors)](https://bundlephobia.com/package/@adobe/leonardo-contrast-colors)
|
7 | [![license](https://img.shields.io/github/license/adobe/leonardo)](https://github.com/adobe/leonardo/blob/master/LICENSE)
|
8 | ![Libraries.io dependency status for latest release, scoped npm package](https://img.shields.io/librariesio/release/npm/@adobe/leonardo-contrast-colors) [![license](https://img.shields.io/github/license/adobe/leonardo)](https://github.com/adobe/leonardo/blob/master/LICENSE)
|
9 | [![Pull requests welcome](https://img.shields.io/badge/PRs-welcome-blueviolet)](https://github.com/adobe/leonardo/blob/master/.github/CONTRIBUTING.md)
|
10 |
|
11 | This package contains all the functions for generating colors by target contrast ratio.
|
12 |
|
13 | ## Using Leonardo Contrast Colors
|
14 |
|
15 | ### Install the package:
|
16 |
|
17 | ```
|
18 | npm i @adobe/leonardo-contrast-colors
|
19 | ```
|
20 |
|
21 | ### Import the package:
|
22 |
|
23 | #### CJS (Node 12.x)
|
24 |
|
25 | ```js
|
26 | const { Theme, Color, BackgroundColor } = require('@adobe/leonardo-contrast-colors');
|
27 | ```
|
28 |
|
29 | #### ESM (Node 13.x)
|
30 |
|
31 | ```js
|
32 | import { Theme, Color, BackgroundColor } from '@adobe/leonardo-contrast-colors';
|
33 | ```
|
34 |
|
35 | ### Create and pass colors and a background color to a new Theme (see additional options below):
|
36 |
|
37 | ```js
|
38 | let gray = new BackgroundColor({
|
39 | name: 'gray',
|
40 | colorKeys: ['#cacaca'],
|
41 | ratios: [2, 3, 4.5, 8]
|
42 | });
|
43 |
|
44 | let blue = new Color({
|
45 | name: 'blue',
|
46 | colorKeys: ['#5CDBFF', '#0000FF'],
|
47 | ratios: [3, 4.5]
|
48 | });
|
49 |
|
50 | let red = new Color({
|
51 | name: 'red',
|
52 | colorKeys: ['#FF9A81', '#FF0000'],
|
53 | ratios: [3, 4.5]
|
54 | });
|
55 |
|
56 | let theme = new Theme({colors: [gray, blue, red], backgroundColor: gray, lightness: 97});
|
57 |
|
58 | // returns theme colors as JSON
|
59 | let colors = theme.contrastColors;
|
60 | ```
|
61 |
|
62 | ## API Reference
|
63 |
|
64 | ### `Theme`
|
65 |
|
66 | Class function used to generate adaptive contrast-based colors. Parameters are destructured and need to be explicitly called.
|
67 |
|
68 | | Parameter | Type | Description |
|
69 | |-----------|-------|------------|
|
70 | | `colors` | Array | List of `Color` classes to generate theme colors for. A single `BackgroundColor` class is required. |
|
71 | | `lightness` | Number | Value from 0-100 for desired lightness of generated theme background color (whole number)|
|
72 | | `contrast` | Number | Multiplier to increase or decrease contrast for all theme colors (default is `1`) |
|
73 | | `output` | Enum | Desired color output format |
|
74 |
|
75 |
|
76 | #### Setters
|
77 | | Setter | Description of output |
|
78 | |--------|-----------------------|
|
79 | | `.lightness()` | Sets the theme's lightness value |
|
80 | | `.contrast()` | Sets the theme's contrast value |
|
81 | | `.backgroundColor()` | Sets the theme's background color (creates a new `BackgroundColor` if passing a string) |
|
82 | | `.colors()` | Sets colors for theme (must pass `Color`)|
|
83 | | `.output()` | Sets output format for theme |
|
84 |
|
85 |
|
86 | #### Supported output formats:
|
87 | Available output formats conform to the [W3C CSS Color Module Level 4](https://www.w3.org/TR/css-color-4/) spec for the supported options, as listed below:
|
88 |
|
89 | | Output option | Sample value |
|
90 | |---------------|--------------|
|
91 | | `'HEX'` _(default)_ | `#RRGGBB` |
|
92 | | `'RGB'` | `rgb(255, 255, 255)` |
|
93 | | `'HSL'` | `hsl(360deg, 0%, 100%)` |
|
94 | | `'HSV'` | `hsv(360deg, 0%, 100%)` |
|
95 | | `'HSLuv'` | `hsluv(360, 0, 100)` |
|
96 | | `'LAB'` | `lab(100%, 0, 0)` |
|
97 | | `'LCH'` | `lch(100%, 0, 360deg)` |
|
98 | | `'CAM02'` | `jab(100%, 0, 0)`|
|
99 | | `'CAM02p'` | `jch(100%, 0, 360deg)` |
|
100 |
|
101 | ----------
|
102 |
|
103 | ### `Color`
|
104 | Class function used to define colors for a theme. Parameters are destructured and need to be explicitly called.
|
105 |
|
106 | | Parameter | Type | Description |
|
107 | |-----------|-------|------|
|
108 | | `name` | String | User-defined name for a color, (eg "Blue"). Used to name output color values |
|
109 | | `colorKeys` | Array of strings | List of specific colors to interpolate between in order to generate a full lightness scale of the color. |
|
110 | | `colorspace` | Enum | The [colorspace](#Supported-interpolation-colorspaces) in which the key colors will be interpolated within. |
|
111 | | `ratios` | Array or Object | List of target contrast ratios, or object with named keys for each value. |
|
112 | | `smooth` | Boolean | Applies bezier smoothing to interpolation (false by default) |
|
113 | | `output` | Enum | Desired color output format |
|
114 |
|
115 | #### Setters
|
116 | | Setter | Description of output |
|
117 | |--------|-----------------------|
|
118 | | `.colorKeys()` | Sets the color keys |
|
119 | | `.colorspace()` | Sets the interpolation colorspace |
|
120 | | `.ratios()` | Sets the ratios |
|
121 | | `.name()` | Sets the name |
|
122 | | `.smooth()` | Sets the smoothing option |
|
123 | | `.output()` | Sets the output format |
|
124 |
|
125 | #### Supported interpolation colorspaces:
|
126 | Below are the available options for interpolation in Leonardo:
|
127 |
|
128 | - [LCH](https://en.wikipedia.org/wiki/HCL_color_space)
|
129 | - [LAB](https://en.wikipedia.org/wiki/CIELAB_color_space)
|
130 | - [CAM02](https://en.wikipedia.org/wiki/CIECAM02)
|
131 | - [HSL](https://en.wikipedia.org/wiki/HSL_and_HSV)
|
132 | - [HSLuv](https://en.wikipedia.org/wiki/HSLuv)
|
133 | - [HSV](https://en.wikipedia.org/wiki/HSL_and_HSV)
|
134 | - [RGB](https://en.wikipedia.org/wiki/RGB_color_space)
|
135 |
|
136 | #### Ratios as an array
|
137 | When passing a flat array of target ratios, the output colors in your Theme will be generated by concatenating the color name (eg "Blue") with numeric increments. Colors with a **positive contrast ratio** with the base (ie, 2:1) will be named in increments of 100. For example, `gray100`, `gray200`.
|
138 |
|
139 | Colors with a **negative contrast ratio** with the base (ie -2:1) will be named in increments less than 100 and _based on the number of negative values declared_. For example, if there are 3 negative values `[-1.4, -1.3, -1.2, 1, 2, 3]`, the name for those values will be incremented by 100/4 (length plus one to avoid a `0` value), such as `gray25`, `gray50`, and `gray75`.
|
140 |
|
141 | For example:
|
142 | ```js
|
143 | new Color({
|
144 | name: 'blue',
|
145 | colorKeys: ['#5CDBFF', '#0000FF'],
|
146 | colorSpace: 'LCH',
|
147 | ratios: [3, 4.5]
|
148 | });
|
149 |
|
150 | // Returns:
|
151 | [
|
152 | {
|
153 | name: 'blue',
|
154 | values: [
|
155 | {name: "blue100", contrast: 3, value: "#8d63ff"},
|
156 | {name: "blue200", contrast: 4.5, value: "#623aff"}
|
157 | ]
|
158 | }
|
159 | ]
|
160 | ```
|
161 |
|
162 | #### Ratios as an object
|
163 | When defining ratios as an object with key-value pairs, you define what name will be output in your Leonardo theme.
|
164 | ```js
|
165 | new Color({
|
166 | name: 'blue',
|
167 | colorKeys: ['#5CDBFF', '#0000FF'],
|
168 | colorSpace: 'LCH',
|
169 | ratios: {
|
170 | 'blue--largeText': 3,
|
171 | 'blue--normalText': 4.5
|
172 | }
|
173 | });
|
174 |
|
175 | // Returns:
|
176 | [
|
177 | {
|
178 | name: 'blue',
|
179 | values: [
|
180 | {name: "blue--largeText", contrast: 3, value: "#8d63ff"},
|
181 | {name: "blue--normalText", contrast: 4.5, value: "#623aff"}
|
182 | ]
|
183 | }
|
184 | ]
|
185 | ```
|
186 |
|
187 | ---
|
188 |
|
189 | ## Output examples
|
190 | There are two types of output you can get from the `Theme` class:
|
191 | | Getter | Description of output |
|
192 | |--------|-----------------------|
|
193 | | `Theme.contrastColors` | Returns array of color objects with key-value pairs |
|
194 | | `Theme.contrastColorPairs` | Returns object with key-value pairs |
|
195 | | `Theme.contrastColorValues` | Returns flat array of color values |
|
196 |
|
197 |
|
198 | ### `Theme.contrastColors`
|
199 | Each color is an object named by user-defined value (eg `name: 'gray'`). "Values" array consists of all generated color values for the color, with properties `name`, `contrast`, and `value`:
|
200 |
|
201 | ```js
|
202 | [
|
203 | { background: "#e0e0e0" },
|
204 | {
|
205 | name: 'gray',
|
206 | values: [
|
207 | {name: "gray100", contrast: 1, value: "#e0e0e0"},
|
208 | {name: "gray200", contrast: 2, value: "#a0a0a0"},
|
209 | {name: "gray300", contrast: 3, value: "#808080"},
|
210 | {name: "gray400", contrast: 4.5, value: "#646464"}
|
211 | ]
|
212 | },
|
213 | {
|
214 | name: 'blue',
|
215 | values: [
|
216 | {name: "blue100", contrast: 2, value: "#b18cff"},
|
217 | {name: "blue200", contrast: 3, value: "#8d63ff"},
|
218 | {name: "blue300", contrast: 4.5, value: "#623aff"},
|
219 | {name: "blue400", contrast: 8, value: "#1c0ad1"}
|
220 | ]
|
221 | }
|
222 | ]
|
223 | ```
|
224 |
|
225 | ### `Theme.contrastColorPairs`
|
226 | Simplified format as an object of key-value pairs. Property is equal to the [generated](#Ratios-as-an-array) or [user-defined name](#Ratios-as-an-object) for each generated value.
|
227 |
|
228 | ```js
|
229 | {
|
230 | "gray100": "#e0e0e0";
|
231 | "gray200": "#a0a0a0";
|
232 | "gray300": "#808080";
|
233 | "gray400": "#646464";
|
234 | "blue100": "#b18cff";
|
235 | "blue200": "#8d63ff";
|
236 | "blue300": "#623aff";
|
237 | "blue400": "#1c0ad1";
|
238 | }
|
239 | ```
|
240 |
|
241 | ### `Theme..contrastColorValues`
|
242 | Returns all color values in a flat array.
|
243 |
|
244 | ```js
|
245 | [
|
246 | "#e0e0e0",
|
247 | "#a0a0a0",
|
248 | "#808080",
|
249 | "#646464",
|
250 | "#b18cff",
|
251 | "#8d63ff",
|
252 | "#623aff",
|
253 | "#1c0ad1"
|
254 | ]
|
255 | ```
|
256 |
|
257 | ---
|
258 |
|
259 | ## Leonardo with CSS variables
|
260 | Here are a few examples of how you can utilize Leonardo to dynamically create or modify CSS variables for your application.
|
261 |
|
262 | ### Vanilla JS
|
263 | ```js
|
264 | let varPrefix = '--';
|
265 |
|
266 | // Iterate each color object
|
267 | for (let i = 0; i < myTheme.length; i++) {
|
268 | // Iterate each value object within each color object
|
269 | for(let j = 0; j < myTheme[i].values.length; j++) {
|
270 | // output "name" of color and prefix
|
271 | let key = myTheme[i].values[j].name;
|
272 | let prop = varPrefix.concat(key);
|
273 | // output value of color
|
274 | let value = myTheme[i].values[j].value;
|
275 | // create CSS property with name and value
|
276 | document.documentElement.style
|
277 | .setProperty(prop, value);
|
278 | }
|
279 | }
|
280 | ```
|
281 |
|
282 | ### React
|
283 | Create a new Theme component `Theme.js` with your parameters:
|
284 | ```js
|
285 | import * as Leo from '@adobe/leonardo-contrast-colors';
|
286 |
|
287 | const Theme = () => {
|
288 | let gray = new Leo.BackgroundColor({
|
289 | name: 'gray',
|
290 | colorKeys: ['#cacaca'],
|
291 | ratios: [2, 3, 4.5, 8]
|
292 | });
|
293 |
|
294 | let blue = new Leo.Color({
|
295 | name: 'blue',
|
296 | colorKeys: ['#5CDBFF', '#0000FF'],
|
297 | ratios: [3, 4.5]
|
298 | });
|
299 |
|
300 | let red = new Leo.Color({
|
301 | name: 'red',
|
302 | colorKeys: ['#FF9A81', '#FF0000'],
|
303 | ratios: [3, 4.5]
|
304 | });
|
305 |
|
306 | const adaptiveTheme = new Leo.Theme({
|
307 | colors: [
|
308 | gray,
|
309 | blue,
|
310 | red
|
311 | ],
|
312 | backgroundColor: gray,
|
313 | lightness: 97,
|
314 | contrast: 1,
|
315 | });
|
316 |
|
317 | return adaptiveTheme;
|
318 | }
|
319 |
|
320 | export default Theme;
|
321 | ```
|
322 | Then import your Theme component at the top level of your application, and pass the Theme as a property of your app:
|
323 |
|
324 | ```js
|
325 | // index.js
|
326 | import Theme from './components/Theme';
|
327 |
|
328 | ReactDOM.render(
|
329 | <React.StrictMode>
|
330 | <App adaptiveTheme={Theme()}/>
|
331 | </React.StrictMode>,
|
332 | document.getElementById('root')
|
333 | );
|
334 | ```
|
335 |
|
336 | In your App.js file, import `useTheme` from `css-vars-hook` and provide the following within your App function in order to format Leonardo's output in the structure required for `css-vars-hook`.
|
337 | ```js
|
338 | // App.js
|
339 | import {useTheme} from 'css-vars-hook';
|
340 |
|
341 | function App(props) {
|
342 | const [lightness, setLightness] = useState(100);
|
343 | const [contrast, setContrast] = useState(1);
|
344 |
|
345 | const _createThemeObject = () => {
|
346 | let themeObj = {}
|
347 | props.adaptiveTheme.contrastColors.forEach(color => {
|
348 | if(color.name) {
|
349 | let values = color.values;
|
350 | values.forEach(instance => {
|
351 | let name = instance.name;
|
352 | let val = instance.value;
|
353 | themeObj[name] = val;
|
354 | });
|
355 | } else {
|
356 | // must be the background
|
357 | let name = 'background'
|
358 | let val = color.background;
|
359 | themeObj[name] = val;
|
360 | }
|
361 | })
|
362 | return themeObj;
|
363 | };
|
364 |
|
365 | const theme = useState( _createThemeObject() );
|
366 |
|
367 | const {setRef, setVariable} = useTheme(theme);
|
368 |
|
369 | return (
|
370 | <div
|
371 | className="App"
|
372 | ref={setRef}
|
373 | >
|
374 | </div>
|
375 | )
|
376 | }
|
377 |
|
378 | ```
|
379 | To make your application adaptive, include a function for updating your theme before your return function:
|
380 |
|
381 | ```js
|
382 | function _updateColorVariables() {
|
383 | let themeInstance = _createThemeObject();
|
384 |
|
385 | for (const [key, value] of Object.entries( themeInstance )) {
|
386 | setVariable(key, value);
|
387 | }
|
388 | };
|
389 | // call function to set initial values
|
390 | _updateColorVariables();
|
391 | ```
|
392 |
|
393 | Finally, reference this function and set the theme parameters when your users interact with slider components (do the same for Contrast):
|
394 | ```js
|
395 | <label htmlFor="lightness">
|
396 | Lightness
|
397 | </label>
|
398 | <input
|
399 | value={lightness}
|
400 | id="lightness"
|
401 | type="range"
|
402 | min={ sliderMin }
|
403 | max={ sliderMax }
|
404 | step="1"
|
405 | onChange={e => {
|
406 | setLightness(e.target.value)
|
407 | props.adaptiveTheme.lightness = e.target.value
|
408 | _updateColorVariables()
|
409 | }}
|
410 | />
|
411 | <label htmlFor="contrast">
|
412 | Contrast
|
413 | </label>
|
414 | <input
|
415 | value={contrast}
|
416 | id="contrast"
|
417 | type="range"
|
418 | min="0.25"
|
419 | max="3"
|
420 | step="0.025"
|
421 | onChange={e => {
|
422 | setContrast(e.target.value)
|
423 | props.adaptiveTheme.contrast = e.target.value
|
424 | _updateColorVariables()
|
425 | }}
|
426 | />
|
427 |
|
428 | ```
|
429 |
|
430 | ### Dark mode support in React
|
431 | Include the following in your App.js file to listen for dark mode. This will pass a different lightness value (of your choice) to Leonardo. It's recommended to restrict the lightness range based on mode in order to avoid inaccessible ranges and to provide a better overall experience
|
432 | ```js
|
433 | const mq = window.matchMedia('(prefers-color-scheme: dark)');
|
434 | // Update lightness and slider min/max to be conditional:
|
435 | const [lightness, setLightness] = useState((mq.matches) ? 8 : 100);
|
436 | const [sliderMin, setSliderMin] = useState((mq.matches) ? 0 : 80);
|
437 | const [sliderMax, setSliderMax] = useState((mq.matches) ? 30 : 100);
|
438 |
|
439 | // Listener to update when user device mode changes:
|
440 | mq.addEventListener('change', function (evt) {
|
441 | props.adaptiveTheme.lightness = ((mq.matches) ? 11 : 100)
|
442 | setLightness((mq.matches) ? 11 : 100)
|
443 | setSliderMin((mq.matches) ? 0 : 80);
|
444 | setSliderMax((mq.matches) ? 30 : 100);
|
445 | });
|
446 | ```
|
447 | ---
|
448 |
|
449 | ## Why are not all contrast ratios available?
|
450 | You may notice the tool takes an input (target ratio) but most often outputs a contrast ratio slightly higher. This has to do with the available colors in the RGB color space, and the math associated with calculating these ratios.
|
451 |
|
452 | For example let's look at blue and white.
|
453 | Blue: rgb(0, 0, 255)
|
454 | White: rgb(255, 255, 255)
|
455 | Contrast ratio: **8.59**:1
|
456 |
|
457 | If we change any one value in the RGB channel for either color, the ratio changes:
|
458 | Blue: rgb(0, **1**, 255)
|
459 | White: rgb(255, 255, 255)
|
460 | Contrast ratio: **8.57**:1
|
461 |
|
462 | If 8.58 is input as the target ratio with the starting color of blue, the output will not be exact. This is exaggerated by the various colorspace interpolations.
|
463 |
|
464 | Since the WCAG requirement is defined as a *minimum contrast requirement*, it should be fine to generate colors that are a little *more* accessible than the minimum.
|
465 |
|
466 |
|
467 | ---
|
468 |
|
469 | ## Chroma.js
|
470 | This project is currently built using [Chroma.js](https://gka.github.io/chroma.js/) with custom extensions to support[CIE CAM02](https://gramaz.io/d3-cam02/). Additional functionality is added in Leonardo to enhance chroma scales so that they properly order colors by lightness and correct the lightness of the scale based on HSLuv.
|
471 |
|
472 | ## Contributing
|
473 | Contributions are welcomed! Read the [Contributing Guide](../../.github/CONTRIBUTING.md) for more information.
|
474 |
|
475 | ## Development
|
476 |
|
477 | You can run tests and watch for changes with:
|
478 |
|
479 | ```sh
|
480 | yarn dev
|
481 | ```
|
482 |
|
483 | ## Licensing
|
484 | This project is licensed under the Apache V2 License. See [LICENSE](LICENSE) for more information.
|