UNPKG

15.3 kBMarkdownView Raw
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
11This 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```
18npm i @adobe/leonardo-contrast-colors
19```
20
21### Import the package:
22
23#### CJS (Node 12.x)
24
25```js
26const { Theme, Color, BackgroundColor } = require('@adobe/leonardo-contrast-colors');
27```
28
29#### ESM (Node 13.x)
30
31```js
32import { 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
38let gray = new BackgroundColor({
39 name: 'gray',
40 colorKeys: ['#cacaca'],
41 ratios: [2, 3, 4.5, 8]
42 });
43
44let blue = new Color({
45 name: 'blue',
46 colorKeys: ['#5CDBFF', '#0000FF'],
47 ratios: [3, 4.5]
48 });
49
50let red = new Color({
51 name: 'red',
52 colorKeys: ['#FF9A81', '#FF0000'],
53 ratios: [3, 4.5]
54 });
55
56let theme = new Theme({colors: [gray, blue, red], backgroundColor: gray, lightness: 97});
57
58// returns theme colors as JSON
59let colors = theme.contrastColors;
60```
61
62## API Reference
63
64### `Theme`
65
66Class 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:
87Available 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`
104Class 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:
126Below 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
137When 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
139Colors 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
141For example:
142```js
143new 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
163When defining ratios as an object with key-value pairs, you define what name will be output in your Leonardo theme.
164```js
165new 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
190There 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`
199Each 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`
226Simplified 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`
242Returns 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
260Here 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
264let varPrefix = '--';
265
266// Iterate each color object
267for (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
283Create a new Theme component `Theme.js` with your parameters:
284```js
285import * as Leo from '@adobe/leonardo-contrast-colors';
286
287const 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
320export default Theme;
321```
322Then 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
326import Theme from './components/Theme';
327
328ReactDOM.render(
329 <React.StrictMode>
330 <App adaptiveTheme={Theme()}/>
331 </React.StrictMode>,
332 document.getElementById('root')
333);
334```
335
336In 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
339import {useTheme} from 'css-vars-hook';
340
341function 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```
379To 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
393Finally, 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
431Include 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
433const mq = window.matchMedia('(prefers-color-scheme: dark)');
434// Update lightness and slider min/max to be conditional:
435const [lightness, setLightness] = useState((mq.matches) ? 8 : 100);
436const [sliderMin, setSliderMin] = useState((mq.matches) ? 0 : 80);
437const [sliderMax, setSliderMax] = useState((mq.matches) ? 30 : 100);
438
439// Listener to update when user device mode changes:
440mq.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?
450You 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
452For example let's look at blue and white.
453Blue: rgb(0, 0, 255)
454White: rgb(255, 255, 255)
455Contrast ratio: **8.59**:1
456
457If we change any one value in the RGB channel for either color, the ratio changes:
458Blue: rgb(0, **1**, 255)
459White: rgb(255, 255, 255)
460Contrast ratio: **8.57**:1
461
462If 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
464Since 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
470This 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
473Contributions are welcomed! Read the [Contributing Guide](../../.github/CONTRIBUTING.md) for more information.
474
475## Development
476
477You can run tests and watch for changes with:
478
479```sh
480yarn dev
481```
482
483## Licensing
484This project is licensed under the Apache V2 License. See [LICENSE](LICENSE) for more information.