Leonardo JS API: Generating Accessible Color Palettes

The API documentation below will guide you in utilizing the @adobe/leonardo-contrast-colors library within your development projects. This powerful tool allows you to effortlessly Compare Hex Colors and generate accessible and harmonious color palettes based on contrast ratios.

Quick Start

Dive straight into using Leonardo with these simple steps.

Installation

Begin by installing the package into your project using npm:

npm install @adobe/leonardo-contrast-colors

Importing the Package

Choose the import method that suits your environment:

CommonJS (CJS) – Node 12.x

const { Theme, Color, BackgroundColor } = require('@adobe/leonardo-contrast-colors');

ECMAScript Modules (ESM) – Node 13.x and later

import { Theme, Color, BackgroundColor } from '@adobe/leonardo-contrast-colors';

Creating a Theme

To start generating colors, you need to define your color scheme. This involves creating Color and BackgroundColor objects and passing them to a new Theme instance.

let gray = new BackgroundColor({ name: 'gray', colorKeys: ['#cacaca'], ratios: [2, 3, 4.5, 8] });
let blue = new Color({ name: 'blue', colorKeys: ['#5CDBFF', '#0000FF'], ratios: [3, 4.5] });
let red = new Color({ name: 'red', colorKeys: ['#FF9A81', '#FF0000'], ratios: [3, 4.5] });
let theme = new Theme({colors: [gray, blue, red], backgroundColor: gray, lightness: 97});

// Retrieve theme colors as JSON
let colors = theme.contrastColors;

API Reference

Explore the detailed API reference to understand the functionalities of the Leonardo library.

Theme Class

The Theme class is central to generating adaptive contrast-based color palettes. It accepts several parameters to customize your theme.

Parameter Type Description
colors Array An array of Color classes that define the colors for your theme. Must include at least one BackgroundColor class.
lightness Number A value between 0 and 100 representing the desired lightness of the generated background color. Must be a whole number.
contrast Number A multiplier to adjust the overall contrast of all theme colors. The default value is 1.
saturation Number A value between 0 and 100 to control the saturation of all theme colors. The default is 100 (no desaturation).
output Enum Specifies the desired color output format (e.g., HEX, RGB, HSL).

Theme Setters

These setters allow you to dynamically modify properties of an existing Theme instance.

Setter Description
Theme.lightness Sets the lightness value for the theme’s background color.
Theme.contrast Adjusts the contrast multiplier for all colors in the theme.
Theme.saturation Modifies the saturation level for all colors in the theme.
Theme.backgroundColor Updates the background color of the theme. Accepts either a BackgroundColor class or a hex color string (which will create a new BackgroundColor).
Theme.colors Replaces the entire array of colors in the theme. Must be an array of Color classes.
Theme.output Changes the output format for all colors in the theme.
Theme.addColor Adds a new Color to the existing theme.
Theme.removeColor Removes a Color from the theme.
Theme.updateColor Modifies properties of an existing Color within the theme using its setters.

Theme.addColor = color

This setter allows you to add a new Color object to your existing theme, expanding your color palette dynamically.

let red = new Color({ name: 'red', colorKeys: ['#FF9A81', '#FF0000'], ratios: [3, 4.5] });
theme.addColor = red;

Theme.removeColor = color

Remove a Color from the theme. You can remove a color either by providing an object with the Color‘s name or by passing the Color class instance itself.

// Remove by color name
theme.removeColor = {name: 'Red'};

// Remove by Color class
const red = new Color({ name: 'red', colorKeys: ['#FF9A81', '#FF0000'], ratios: [3, 4.5] });
theme.removeColor = red;

Theme.updateColor = {name, property}

Modify an existing Color within the theme by using this setter. You need to specify the name of the color you want to update and the property you wish to change along with its new value.

// Change the color ratios
theme.updateColor = {color: 'red', ratios: [3, 4.5, 7]};

// Change the color colorKeys
theme.updateColor = {color: 'red', colorKeys: ['#ff0000']};

// Change the color's name
theme.updateColor = {color: 'red', name: 'Crimson'};

You can also update multiple properties of a color in a single call.

// Change ratios, colorKeys, and name simultaneously
theme.updateColor = {color: 'red', ratios: [3, 4.5, 7], colorKeys: ['#ff0000'], name: 'Crimson'};

Supported Output Formats

Leonardo supports a wide range of output formats, adhering to the W3C CSS Color Module Level 4 specification.

Output option Sample value
'HEX' (default) #RRGGBB
'RGB' rgb(255, 255, 255)
'HSL' hsl(360deg, 0%, 100%)
'HSV' hsv(360deg, 0%, 100%)
'HSLuv' hsluv(360, 0, 100)
'LAB' lab(100%, 0, 0)
'LCH' lch(100%, 0, 360deg)
'CAM02' jab(100%, 0, 0)
'CAM02p' jch(100%, 0, 360deg)

Color Class

The Color class is used to define individual colors within your theme. It allows you to specify color keys, interpolation colorspaces, and target contrast ratios, enabling you to create a nuanced color scale.

Parameter Type Description
name String A user-defined name for the color (e.g., “Blue”). This name is used to identify the color in the output.
colorKeys Array of strings An array of hex color codes that serve as key points for color interpolation. Leonardo will generate a continuous color scale between these keys.
colorspace Enum The colorspace used for interpolation between the colorKeys. See Supported Interpolation Colorspaces for options.
ratios Array or Object Defines the target contrast ratios for generating color variations. Can be an array of ratios or an object for named ratios.
smooth Boolean When set to true, applies bezier smoothing to the color interpolation for a more gradual transition. Defaults to false.
output Enum Specifies the desired color output format for this specific color, overriding the theme’s output format if set.

Color Setters

These setters enable dynamic modification of Color object properties.

Setter Description
Color.colorKeys Updates the array of color keys used for interpolation.
Color.colorspace Changes the interpolation colorspace.
Color.ratios Modifies the target contrast ratios.
Color.name Renames the color.
Color.smooth Toggles bezier smoothing on or off.
Color.output Sets the output format for the color.

Supported Interpolation Colorspaces

Leonardo offers a variety of colorspaces for color interpolation, allowing you to fine-tune how colors transition within your palettes.

  • 'CAM02'
  • 'CAM02p'
  • 'LAB'
  • 'LCH'
  • 'HSLuv'
  • 'HSL'
  • 'HSV'
  • 'RGB'

Ratios as an Array

When you provide ratios as a simple array, Leonardo automatically generates names for the output colors by appending numerical increments to the base color name. Positive contrast ratios (greater than 1:1 with the background) are incremented in multiples of 100 (e.g., gray100, gray200).

Negative contrast ratios (less than 1:1 with the background) are also numerically incremented, but the increment is calculated based on the number of negative ratios provided. For instance, with ratios [-1.4, -1.3, -1.2, 1, 2, 3], negative values will be named with increments of 100 divided by the number of negative values plus one (in this case, 100/4), leading to names like gray25, gray50, and gray75.

new Color({
  name: 'blue',
  colorKeys: ['#5CDBFF', '#0000FF'],
  colorSpace: 'LCH',
  ratios: [3, 4.5]
});
// Returns:
[
  {
    name: 'blue',
    values: [
      {name: "blue100", contrast: 3, value: "#8d63ff"},
      {name: "blue200", contrast: 4.5, value: "#623aff"}
    ]
  }
]

Ratios as an Object

For more control over naming, you can define ratios as an object with key-value pairs. The keys in the object will be used as the names for the generated color variations in your Leonardo theme. This is especially useful when you need semantic names that describe the color’s purpose, such as for different text sizes or UI elements. This allows for a more intuitive way to compare hex colors assigned to specific UI roles.

new Color({
  name: 'blue',
  colorKeys: ['#5CDBFF', '#0000FF'],
  colorSpace: 'LCH',
  ratios: {
    'blue--largeText': 3,
    'blue--normalText': 4.5
  }
});
// Returns:
[
  {
    name: 'blue',
    values: [
      {name: "blue--largeText", contrast: 3, value: "#8d63ff"},
      {name: "blue--normalText", contrast: 4.5, value: "#623aff"}
    ]
  }
]

Output Examples

The Theme class provides different ways to access the generated color values, catering to various use cases.

Getter Description of output
Theme.contrastColors Returns an array of color objects, each containing key-value pairs with color details.
Theme.contrastColorPairs Provides a simplified object with key-value pairs where keys are color names and values are hex color codes.
Theme.contrastColorValues Returns a flat array containing only the generated hex color values.

Theme.contrastColors

This getter returns a structured array where each element represents a color defined in your theme. Each color object includes a values array, listing all generated color variations with their name, contrast ratio, and value (in the specified output format, HEX by default). The background color is also included as the first element in the array.

[
  { background: "#e0e0e0" },
  {
    name: 'gray',
    values: [
      {name: "gray100", contrast: 1, value: "#e0e0e0"},
      {name: "gray200", contrast: 2, value: "#a0a0a0"},
      {name: "gray300", contrast: 3, value: "#808080"},
      {name: "gray400", contrast: 4.5, value: "#646464"}
    ]
  },
  {
    name: 'blue',
    values: [
      {name: "blue100", contrast: 2, value: "#b18cff"},
      {name: "blue200", contrast: 3, value: "#8d63ff"},
      {name: "blue300", contrast: 4.5, value: "#623aff"},
      {name: "blue400", contrast: 8, value: "#1c0ad1"}
    ]
  }
]

Theme.contrastColorPairs

For a simpler output, contrastColorPairs returns an object where keys are the generated color names (either numeric or user-defined) and values are the corresponding hex color codes. This format is ideal for direct use in CSS or JavaScript applications where you need to quickly access colors by name. This makes it easy to compare hex colors by their assigned names.

{
  "gray100": "#e0e0e0",
  "gray200": "#a0a0a0",
  "gray300": "#808080",
  "gray400": "#646464",
  "blue100": "#b18cff",
  "blue200": "#8d63ff",
  "blue300": "#623aff",
  "blue400": "#1c0ad1",
}

Theme.contrastColorValues

If you only need a flat array of hex color values without names or contrast information, contrastColorValues provides just that. This can be useful for quickly iterating through all generated colors or when the color order is sufficient for your needs.

[
  "#e0e0e0",
  "#a0a0a0",
  "#808080",
  "#646464",
  "#b18cff",
  "#8d63ff",
  "#623aff",
  "#1c0ad1"
]

Leonardo with CSS Variables

Leonardo can be seamlessly integrated with CSS variables, enabling dynamic theme updates in your application. Here are examples for Vanilla JS and React.

Vanilla JS

This example demonstrates how to dynamically create CSS variables from a Leonardo theme in Vanilla JavaScript.

let varPrefix = '--';

// Iterate through each color object
for (let i = 0; i < myTheme.length; i++) {
  // Iterate through each value object within each color object
  for(let j = 0; j < myTheme[i].values.length; j++) {
    // Output "name" of color and prefix
    let key = myTheme[i].values[j].name;
    let prop = varPrefix.concat(key);

    // Output value of color
    let value = myTheme[i].values[j].value;

    // Create CSS property with name and value
    document.documentElement.style.setProperty(prop, value);
  }
}

React

For React applications, you can create a Theme component and utilize the css-vars-hook to manage CSS variables.

Create a Theme.js component:

import * as Leo from '@adobe/leonardo-contrast-colors';

const Theme = () => {
  let gray = new Leo.BackgroundColor({ name: 'gray', colorKeys: ['#cacaca'], ratios: [2, 3, 4.5, 8] });
  let blue = new Leo.Color({ name: 'blue', colorKeys: ['#5CDBFF', '#0000FF'], ratios: [3, 4.5] });
  let red = new Leo.Color({ name: 'red', colorKeys: ['#FF9A81', '#FF0000'], ratios: [3, 4.5] });

  const adaptiveTheme = new Leo.Theme({
    colors: [ gray, blue, red ],
    backgroundColor: gray,
    lightness: 97,
    contrast: 1,
  });

  return adaptiveTheme;
}

export default Theme;

Import the Theme component in your main application file (e.g., index.js):

// index.js
import Theme from './components/Theme';
import ReactDOM from 'react-dom';
import React from 'react';

ReactDOM.render(
  <React.StrictMode>
    <App adaptiveTheme={Theme()} />
  </React.StrictMode>,
  document.getElementById('root')
);

In your App.js file, use the useTheme hook from css-vars-hook to apply the Leonardo theme as CSS variables.

// App.js
import {useTheme} from 'css-vars-hook';
import React, {useState} from 'react';

function App(props) {
  const [lightness, setLightness] = useState(100);
  const [contrast, setContrast] = useState(1);

  const _createThemeObject = () => {
    let themeObj = {}
    props.adaptiveTheme.contrastColors.forEach(color => {
      if(color.name) {
        let values = color.values;
        values.forEach(instance => {
          let name = instance.name;
          let val = instance.value;
          themeObj[name] = val;
        });
      } else {
        // must be the background
        let name = 'background'
        let val = color.background;
        themeObj[name] = val;
      }
    })
    return themeObj;
  };

  const theme = useState( _createThemeObject() );
  const {setRef, setVariable} = useTheme(theme);

  function _updateColorVariables() {
    let themeInstance = _createThemeObject();
    for (const [key, value] of Object.entries( themeInstance )) {
      setVariable(key, value);
    }
  };
  // call function to set initial values
  _updateColorVariables();


  return (
    <div className="App" ref={setRef} >
      <div>
        <label htmlFor="lightness">
          Lightness
          <input value={lightness} id="lightness" type="range" min={ 80 } max={ 100 } step="1" onChange={e => {
            setLightness(e.target.value)
            props.adaptiveTheme.lightness = e.target.value
            _updateColorVariables()
          }} />
        </label>
        <label htmlFor="contrast">
          Contrast
          <input value={contrast} id="contrast" type="range" min="0.25" max="3" step="0.025" onChange={e => {
            setContrast(e.target.value)
            props.adaptiveTheme.contrast = e.target.value
            _updateColorVariables()
          }} />
        </label>
      </div>
    </div>
  )
}

To enable dynamic theme updates based on user interactions (like sliders), include the _updateColorVariables function and call it within your event handlers.

Dark Mode Support in React

Implement dark mode support by listening to the user’s system preference and adjusting the Leonardo theme accordingly.

const mq = window.matchMedia('(prefers-color-scheme: dark)');

// Update lightness and slider min/max to be conditional:
const [lightness, setLightness] = useState((mq.matches) ? 8 : 100);
const [sliderMin, setSliderMin] = useState((mq.matches) ? 0 : 80);
const [sliderMax, setSliderMax] = useState((mq.matches) ? 30 : 100);

// Listener to update when user device mode changes:
mq.addEventListener('change', function (evt) {
  props.adaptiveTheme.lightness = ((mq.matches) ? 11 : 100)
  setLightness((mq.matches) ? 11 : 100)
  setSliderMin((mq.matches) ? 0 : 80);
  setSliderMax((mq.matches) ? 30 : 100);
});

Why are not all contrast ratios available?

You might observe that the API often outputs a contrast ratio slightly higher than the target ratio you input. This is due to the limitations of the RGB color space and the mathematical complexities of contrast ratio calculations.

For instance, consider blue and white:

  • Blue: rgb(0, 0, 255)
  • White: rgb(255, 255, 255)
  • Contrast ratio: 8.59:1

Even a minor change in the RGB value of either color alters the contrast ratio:

  • Blue: rgb(0, 1, 255)
  • White: rgb(255, 255, 255)
  • Contrast ratio: 8.57:1

If you input 8.58 as the target ratio with blue as the starting color, the exact ratio might not be achievable. This discrepancy is further influenced by different colorspace interpolations.

Given that WCAG defines minimum contrast requirements, generating colors that are slightly more accessible than the minimum is considered acceptable and even beneficial for ensuring accessibility.

Chroma.js

Leonardo is built upon Chroma.js, enhanced with custom extensions to support CIE CAM02. Leonardo further refines chroma scales to ensure colors are correctly ordered by lightness and adjusts the lightness of the scale based on HSLuv for improved perceptual uniformity.

Contributing

We welcome contributions to Leonardo! Please read the Contributing Guide for detailed information on how you can contribute.

Development

For local development and testing, you can use the following command to run tests and watch for file changes:

npm run dev

Licensing

Leonardo is licensed under the Apache V2 License. See the LICENSE file for more details.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *