The NSW colour pairing tool is live in the NSWDS app. It started with a practical problem: teams do not need another place to paste two hex values and get a contrast ratio back. They need a system-aware tool that can answer the next question: given this palette, this background and this interface context, which foregrounds are worth using?

The main lesson was not about contrast maths. That part is well-defined. The harder work was deciding what the tool should refuse to do, what it should recommend by default, and how much detail to expose before the interface becomes a spreadsheet with nicer spacing.

Start with the system

Free-form colour pickers are tempting because they look powerful. In a design system, that freedom can become noise. This tool starts from the NSW brand and Aboriginal palettes, asks for a primary and accent family, and always includes grey because neutral colour carries a lot of real interface work.

The implementation follows that decision. buildPaletteFamilies takes the palette data and turns it into UI-ready families, but it only keeps the approved tones the tool is designed around: 200, 400, 600 and 800. If a family does not have that full set, it does not become an option.

That is product design hiding inside a JavaScript function. The data shape prevents the interface from drifting into colours the system is not trying to support. It also makes the recommendation logic smaller, clearer and easier to test.

Rank, do not just validate

Under the interface, the tool uses culori.wcagContrast through a small getContrastRatio helper. From there, buildForegroundCandidates gathers foregrounds from the selected primary family, selected accent family, grey and white, then dedupes repeated hex values so the same colour does not show up twice under different names.

A boolean pass/fail result would have been easy. It also would have stopped too early.scoreForegroundCandidates adds the contrast ratio to every option, then getRecommendedCandidate looks for AAA normal text first and gives a small preference to colours from the primary family. The default recommendation should be strong enough for body copy and still feel like it belongs to the selected colour system.

The tool still needs to surface useful-but-limited pairings. getForegroundOptionsMeetingAaLarge keeps anything that reaches 3:1 and sorts those options by contrast, so a colour can be considered for large text, icons or UI treatment without pretending it is safe for normal body copy.

Make the preview honest

The preview does not render every example all the time. supportsLargeTextPreview controls whether the large heading appears, and supportsNormalTextPreview controls whether body text and button examples appear. If a pairing only passes for large text, the preview narrows to that use case. If it fails, the UI says so instead of dressing it up.

The status labels come from small functions too: getPreviewHeroRatingLabel, getPreviewHeroStatusLabel, getContrastTone and getCompliancePills. They map the raw ratio to AA large, AA normal, AAA large and AAA normal outcomes. Keeping that logic close to the UI matters because the labels, colours and preview behaviour all need to move together.

Make the decision portable

A pairing tool is not finished if the result cannot be shared. The selected palette, primary family, accent family, background and foreground all need to travel with the link. getInitialPairingState reads that state on load, parseSharedPairParam handles compact shared pair values, and updateUrlParams keeps the browser URL in sync as the selection changes.

The copy action follows the same idea. It gives the foreground token and value, background token and value, contrast ratio, and compliance summary. HEX, RGB, HSL and OKLCH are all available because designers and developers do not always need the same representation at the same point in the work.

The useful constraint

The tool is deliberately bounded. It does not let you invent a colour, tune every shade, or optimise a one-off combination outside the NSW palettes. For this product, that is the right tradeoff. The job is not infinite exploration. The job is to make the approved colour system easier to use without making teams manually test every pairing from scratch.

The developer takeaway is simple: when building design-system tooling, start with the tokens, encode the product opinion in small functions, rank useful choices instead of only validating input, and make the selected state easy to share. The best tools do not just expose data. They reduce the number of shaky decisions a team has to make.