Legible
Make colour readable.
What it is
Legible is a colour contrast checker built for actual design work. Most tools make you check two colours at a time, but real projects use a whole palette. Legible lets you paste a CSS file or type in up to twelve colours, and it instantly generates a matrix of every possible pair. It shows you the contrast ratio, whether it passes WCAG standards, and gives you a suggested fix if a pair fails.
I built it with React and plain JavaScript. There is no backend and no API. Every piece of logic, from parsing colour formats to calculating luminance, runs entirely in the browser.
The problem
The European Accessibility Act began applying to covered products and services in June 2025. It made contrast feel like a practical frontend concern to me. But most contrast checkers are built for single pairs. If you have a palette of six colours, you have to manually check fifteen different combinations. And when a pair fails, the tool usually just tells you it failed and leaves you to guess a better shade.
I wanted to build a tool that handled the math for you and treated a palette as a single system rather than a list of isolated hex codes. I also wanted a project where every line of complexity was pure frontend logic, with no database or server to hide behind.
The decisions
Building the matrix
The interface is split into three zones: the palette input, the contrast matrix, and a detail panel for the selected pair. You can type colours in hex, rgb, or hsl, or just paste a whole CSS file and the app pulls out every hex code in the order it appears.
All the shared state lives in a single component. The matrix itself is never actually stored. It is derived on every change from the current list of colours, which means there is no complex state to keep in sync. The selected pair is stored by colour ID, so it survives edits but disappears automatically if you delete one of the colours. The URL acts as the persistence layer. The state is written to the query string, and loading a shared link runs every value through the exact same parser as typed input.
The colour engine and the fix algorithm
Everything that can go wrong with colour parsing lives in a separate folder as pure functions. The parser returns a success object or a specific reason why it failed, like an invalid range or format, so the UI can show a helpful error message. The WCAG math follows the standard exactly, and ratios are always rounded down so a failing ratio of 4.498 never accidentally shows up as a passing 4.50.
When a pair fails, the app suggests a fix. It only changes the lightness of the colour so the hue stays the same. It uses a binary search to find the lightness value closest to the original that still passes the target ratio. Every candidate is checked as a real rounded hex code to guarantee the suggestion actually works, and any shade already in your palette is skipped, since applying it would only create a duplicate. If it has to change the lightness by a massive amount, it warns you that the colour will read as completely different, because a dark gold that passes against light grey is just brown. That is why the panel also lists the colours in your palette that already pass with each one. Often the better fix is a different pairing, not a different shade.
Designing a neutral interface
The interface has no colour of its own, apart from the green, amber and red of the pass and fail badges. Colours are judged in context, and a brightly coloured UI would change how the user perceives their own palette. The visual character comes entirely from the user's colours and the typography. Filled buttons are saved for the main action in each area, containers only get simple hairlines, and the controls get a slightly darker outline because WCAG asks for 3:1 contrast on those, so nothing distracts from the matrix.
Testing and accessibility
A contrast tool has to meet its own standards. The matrix is built as a real HTML table with proper row and column headers, and it uses a roving tabindex so you can navigate the whole grid with a keyboard.
The colour engine and the URL state are covered by 72 unit tests, checking the exact anchors from the WCAG standard, every input syntax, and the edge cases for the fix algorithm. I also audited the interface using Legible itself. The first pass showed that my default muted grey text and the status badges failed the highest AAA standard, so I swapped them for the exact hex codes the tool suggested.
What I would do differently
I would add APCA as a second scoring mode, since it is the newer contrast model being explored for WCAG 3. I would also add support for transparent colours by blending them over a chosen background first before calculating the ratio. Finally, I would write component tests for the keyboard navigation flows so they run automatically with every commit instead of me having to remember to check them manually.