The number you ask for is a floor, not a target
Generate colors based on a desired contrast ratio
At a glance
- What is it?
- Leonardo is Adobe's open source tool for adaptive colour palettes, and its central move is to treat a contrast ratio as an input rather than an audit result: you ask for 4.5 to 1 and it returns a colour that meets it. The interesting parts are the parts the README is candid about, which is that RGB rarely lets you hit an exact number, and the fact that the library is a d3 linear scale with appearance-model modules bolted on.
- Who is it for?
- Adopt Leonardo if you ship an interface with a dark mode or a user-adjustable theme, because it moves contrast from an audit to a constraint and gives end users a control that cannot violate your palette. Do not adopt it if all you need is a contrast checker, since the WCAG formula is a few lines of arithmetic and a library is overkill for that.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 84 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Contrast as an input, not an audit
The stated goal is more specific than accessibility in general. It is to make it easier for designers and engineers to use colour science to build custom interpolations for a value scale, and to make it easier to conform to the WCAG minimum contrast standard by using the contrast ratio as the starting point rather than a post-colour-selection auditing process. The second half is the whole idea, and it is a workflow change rather than a maths change. The old order is pick a brand colour, build a scale, ship it, then have someone find that the secondary text on the panel fails. The order this tool supports is decide that the secondary text must be at least 4.5 to 1 against its background, and let the tool find the colours. That is a small inversion with large practical consequences, because the failing case stops being a defect discovered late and becomes an input nobody can express incorrectly. The web application exists to make that input a visual thing, and the library exists so the same computation can run in your product.
Why 8.58 is not a number you can always have
The README has a section explaining why the tool often returns a ratio slightly higher than the one you asked for, and the example is worth reading closely because it is the kind of thing most colour tools hide. Blue, written as rgb(0, 0, 255), against white at rgb(255, 255, 255) has a contrast ratio of 8.59 to 1. Move blue one step to rgb(0, 1, 255) and the ratio becomes 8.57 to 1. So if you ask for 8.58 with blue as the starting colour, there is no colour that hits it, because the channel values available around blue are not spaced finely enough. The README says this is exaggerated by the various colourspace interpolations, which is the deeper problem, since you are not searching a discrete grid but a continuous path through a colour space, and the ratio along that path moves in steps set by the interpolation. The justification for accepting it is the standard's own wording, since WCAG states a minimum contrast requirement, so a colour slightly more accessible than the minimum is compliant. The practical consequence is that you must treat the output as satisfying the requirement rather than matching your number, and any test you write should assert a floor, never an equality.
D3 was chosen for its colour appearance modules
The library is built on d3-color, and the README is unusually explicit about why it was not the more obvious choice. It compares the functionality to Chroma.js and calls it comparable, then gives the real reason: d3-color has additional modules available for state-of-the-art colour appearance models, naming CIE CAM02. That distinction matters. A colour interpolation function moves between two points in a numeric space and gives you a colour, which is what a design token pipeline needs. A colour appearance model is about how a colour is perceived under a viewing condition, and the module list is what lets this project interpolate in a space that models perception rather than raw channels. The mechanism underneath is small: createScale() is described as basically a wrapper around a d3 linear scale for colours, with a few enhancements that aid the generateContrastColors() function. So the search for a colour that hits a ratio is a scale lookup, not a solver, which is fast and predictable and explains why the tool can be used interactively in a browser.
Adaptive colour is a permissions model
The three Medium articles the README links describe the idea, and the demo application shows what it means in practice. The demo has brightness and contrast controls, and adjusting them regenerates the entire interface palette by calling generateContrastColors(). The point is not the demo. The point is the sentence that follows, that all of the changes to the interface colours are in conformity with the parameters set up by designers in the web application, which is what lets end users have flexibility and control while the design team's colour choices still hold. That makes Leonardo less a colour picker than a constraint system: the designer supplies the variables and the target ratios, the user supplies a preference, and the palette moves inside a boundary the designer drew. It also reframes dark mode. A dark theme stops being a second set of hand-picked values and becomes the same variables evaluated against a different background, which is the version of dark mode that does not rot.
A palette is a URL, and an approved palette is a config
Two features of the web application are workflow details that matter more than they look. The first is that the URL updates with your parameters, so a palette is a link you can send to a colleague rather than a screenshot that gets retyped. The second is that the app displays the specific config parameters when a designer sends you a version they have approved. Put together, that is a small review protocol built out of a query string. A designer builds a scale, checks it, sends the link, and the recipient can read the configuration behind what they are looking at instead of guessing which value is which. Neither feature is in the library, and both are the reason the web tool is worth keeping around even if you only ship the library. The demo and the tool are also separate pages, with the demo at a separate address, so the playground is not the same surface as the authoring tool.
A monorepo with a task runner and a release train
The repository is a pnpm workspace with the machinery of a professional JavaScript project laid out in the file list. There is a changesets directory, which is how versions get cut, and a matching release script that publishes through it. There is a moon directory, because the documented development commands run through moon rather than through npm scripts. There is a husky directory and a lint-staged configuration, so linting runs on staged files before a commit, plus a commitlint configuration for conventional commit messages. The root package is private, the package manager is pinned to a specific pnpm version, Node 24 or newer is required, and pnpm's onlyBuiltDependencies list names the three native packages allowed to run install scripts. Two entries in the root dependencies are polyfills, a buffer implementation, a process implementation and two browser shims for path and the module system, which is the clearest signal in the file that this library is expected to run in a browser as well as in Node.
Getting it running, and an MCP package at 0.1.0
The development instructions are two short sequences. For the interface, install at the root, change into the docs UI directory and start the task runner:
# Install dependencies
pnpm install
# Change directory to Leonardo UI
cd docs/ui/
# Run local server
pnpm moon run devFor the library itself, change into the packages directory and run the package's dev script, which the README describes as running the tests and watching for changes:
cd packages/contrast-colors/
pnpm devBoth end at a live-reloading local site on port 1234, with an index page and the demo page. The release list is where the current shape of the project shows. The library, @adobe/leonardo-contrast-colors, went to 1.0.1 and then 1.1.0 on the same day in February 2026, and three days later a separate package, @adobe/leonardo-mcp, appeared at 0.1.0. The repository also carries a skills directory and an llms.txt file, both of which are conventions for making a project legible to an agent rather than to a browser. Read together, the picture is a mature colour library with a brand new agent-facing surface, and the version numbers say so more clearly than any prose would.
Against computing the ratio yourself, and against a token pipeline
The cheaper alternative for the accessibility half is arithmetic. The WCAG contrast ratio is defined in terms of relative luminance, and a correct implementation with a small perceptual correction for the exponent is a page of code that any competent team can write and test. What you give up is everything else in this repository, because the ratio tells you whether two colours you already have pass, and says nothing about which colours to use or how they should move together. The second alternative is a conventional design token pipeline, where a designer writes scales in a JSON or YAML file and a build step emits CSS custom properties, and the contrast check happens in a lint rule or an audit. That pipeline is a better fit when your palettes are fixed and your users do not get controls. Leonardo fits the other case, where the palette has to be computed at runtime, in the browser, from variables somebody chose. The honest summary is that the WCAG check is trivial, the search for a colour that satisfies a ratio is not, and the governance layer on top, where a user control cannot leave the designer's envelope, is the part no other tool in this space does.
Editorial conclusion
Adopt Leonardo if you ship an interface with a dark mode or a user-adjustable theme, because it moves contrast from an audit to a constraint and gives end users a control that cannot violate your palette. Do not adopt it if all you need is a contrast checker, since the WCAG formula is a few lines of arithmetic and a library is overkill for that. Verify four things before you build on it: what the library actually returns when a ratio is unreachable in the starting colour's gamut, since the documented example overshoots, which appearance model you get by default, because the D3 choice was made for CIE CAM02 support rather than ergonomics, that you are on the contrast-colors package and not the MCP package if you want a stable dependency, since the library is at 1.1.0 while the MCP surface is at 0.1.0, and which Node you build with, since the monorepo requires Node 24 or newer and pnpm 10. The licence is Apache-2.0, the last release was @adobe/[email protected] on 2026-02-21, and the last push was 2026-07-08.
Frequently asked questions
What does Adobe Leonardo do?
It generates adaptive colour palettes from a desired contrast ratio. The goal is to use the contrast ratio as the starting point when building a value scale, rather than picking colours first and auditing them for WCAG minimum contrast afterwards.
Why does Leonardo not always return the exact contrast ratio I ask for?
Because the available colours in RGB space and the math of the ratio do not allow it. The README's example is blue against white at 8.59:1, which becomes 8.57:1 when blue is rgb(0, 1, 255), so a target of 8.58 is unreachable, and colourspace interpolation exaggerates the effect. Since WCAG states a minimum, overshooting is treated as acceptable.
Why does Leonardo use d3-color rather than Chroma.js?
The README says the functionality is comparable, and that the choice is based on the additional d3-color modules available for state-of-the-art colour appearance models such as CIE CAM02. createScale() is a wrapper around a d3 linear colour scale with enhancements for generateContrastColors().
What packages does the Leonardo repository publish?
At least two on npm: @adobe/leonardo-contrast-colors, the library, which reached 1.1.0 in February 2026, and @adobe/leonardo-mcp, which appeared at 0.1.0 three days later. The repository is a private pnpm workspace with the interface and the library under packages.
How do I develop Leonardo locally?
pnpm is required. Install at the repository root, change into docs/ui/ and run pnpm moon run dev for the interface, or change into packages/contrast-colors/ and run pnpm dev for the library tests with watch. Both serve live-reloading pages on port 1234.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/adobe-leonardo)