The NSWDS app now has colour utilities in color-palette.ts and colors.ts. They sit behind the palette pages, theme tools, colour pairing tool, pattern palettes and data exports. The useful lesson was not that tokens should exist. That is table stakes. The useful lesson was how much cleaner the system becomes when the palette source and the transformation logic are not tangled together.
color-palette.ts is the source layer. It defines the base colour sets: brand, Aboriginal, semantic, sequential and diverging palettes. colors.ts is the engine layer. It converts, interpolates, names, formats and exports those colours into the shapes the rest of the app needs.
Keep the source boring
A good token source file should be easy to inspect. The base palettes in color-palette.ts are plain records of named colour arrays. That makes the authoring layer obvious: these are the colours the system starts from, grouped by the way people actually use them.
The authored sets are not all the same kind of colour. Brand palettes, Aboriginal palettes, semantic colours and data visualisation ramps have different jobs. Keeping those groups explicit prevents the rest of the app from treating every colour as if it should behave the same way.
The best practice here is simple: do not make the base palette clever. Make it stable, named and boring. Put the cleverness in functions that can be tested, reused and replaced.
Use one colour space for generation
The generation functions in colors.ts work in OKLCH. oklchConverter turns authored hex values into OKLCH, then interpolateColors, GenerateInterpolatedColors and generateDataVisColors build usable scales from those anchors.
That decision matters. If a design system is going to generate shades, it needs a colour space that behaves closer to how people perceive colour. OKLCH gives the utilities a better basis for interpolation than bouncing between arbitrary hex values and hoping the middle tones feel right.
The extra start and stop colours from addStartStopToColorArray are also a practical detail. Generated scales often need breathing room beyond the authored anchors, especially when the final output is sliced back into named token steps.
Name tokens for use
createColorArray turns generated colour data into token objects with token, oklch, hex, rgb and hsl. It also applies the NSW 01 to 04 names to the 800, 600, 400 and 200 steps.
That is a useful split between machine naming and human naming. Developers need stable tokens like nsw-blue-600. Designers and documentation need labels like NSW Blue 02. Both should come from the same data, or the system starts drifting.
Numeric shade steps are not glamorous, but they are predictable. Predictable beats expressive when tokens need to work across CSS variables, Tailwind output, JSON exports, component props and documentation.
Generate themes from tokens
The generateColorThemes function creates the smaller theme sets from the full palette data. It uses themeIndices and themeTokens to pull out the 200, 400, 600 and 800 steps that theme tools and interface previews rely on.
This is the right kind of constraint. A theme builder does not need every generated shade all the time. It needs the usable decision points: light surface, lighter support, strong brand colour and dark anchor. Smaller theme sets make UI tools easier to scan and harder to misuse.
The lesson is to avoid making components know how to derive their own theme palettes. Components should consume already-shaped theme data. Generation belongs closer to the token layer.
Export for real workflows
Tokens become more useful when they can leave the app cleanly. renderColorOutput supports CSS, SCSS, Less, Tailwind, JSON, JavaScript and TypeScript output. renderColorOutputToDTFM handles a design-token JSON shape with structured colour values when the selected format is not hex.
That sounds like plumbing, but it is product work. A design system has multiple consumers. Some people need CSS variables. Some need Tailwind-compatible custom properties. Some need JSON for token pipelines. Some just need a TypeScript object they can paste into a prototype.
The best practice is not to make one export format sacred. Keep the internal data shape consistent, then render it into the formats teams actually use.
Small helpers matter
A few of the most useful functions are intentionally small. getColorValue gives components one way to read a colour in the selected format. isLightColor gives UI components a quick brightness check. getSurroundingColors helps pull a local range around a theme colour.
These helpers keep display components from becoming token processors. That is the line worth protecting. Components can choose how to present colour, but they should not reinvent how colour data is generated, named or exported.
The useful pattern
The reusable pattern is: author the base palette once, convert it into a rich colour object, generate the scales, name the tokens, derive smaller theme sets, then export the same data into the formats teams need.
That makes the system easier to extend. Adding a palette is mostly data. Improving the scale generation is mostly utility work. Changing an export format does not require rewriting the theme tools. That separation is what keeps design-system colour work from turning into a pile of one-off swatches.