Design systems
Design tokens as one source of truth: giving a Sudoku app a real art direction
A generic Sudoku grid is easy to ship and easy to forget. Giving ours an art direction meant changing the token system first, not the screens.
Most Sudoku apps look the same: a flat gray grid, Roboto numbers, generic Material buttons, everything white and gray. It is safe, and it is forgettable. Our Sudoku had drifted into exactly that look, so we gave it a real art direction. The interesting part was not picking colors. It was that the app already had a theme system and a hard performance budget, and both of them decided how far “bold” could go.
This is the story of that redesign as it actually shipped: a first vertical slice, not a finished app.
Name the direction before touching a screen
We committed to one direction and named it: Editorial Ink. Warm, paper-like neutrals in light mode; a warm charcoal in dark mode; a foreground that is ink-near-black instead of pure black. The single brand color stayed the brand purple. The four paid skins — fun, ocean, forest, sunset — kept working.
Naming it early mattered more than it sounds. “Make it look nicer” produces a hundred small, inconsistent decisions. “Does this belong to Editorial Ink?” produces one. Every later choice — a border weight, a selection tint, a shadow — had something to answer to.
Color is a role, not a hex
The rule in this repo is old and boring: components never hardcode a hex. They consume a semantic token. primary is the brand and the main action. accent is a subtle highlight, not a second brand color. Functional colors like danger are a separate axis with their own foreground pair for contrast.
The redesign forced us to honor that rule where we had cheated. Two examples:
accenthad quietly become a second brand color — an ochre that competed with the purple. We reclassified it to a subtle neutral highlight and removed the ochre.- The Skia board still drew errors with a hardcoded
#dc2626. That red ignored the theme entirely. We promoteddanger/dangerForegroundto real theme-aware tokens and deleted the literal.
We also added the neutrals the new look needed as first-class tokens (secondary, muted) and renamed foregroundMuted to mutedForeground so the app and the website use the same words. Every token was propagated to all six themes and to the CSS variables, then mapped in the Tailwind config so NativeWind classes like bg-danger or text-cell-highlight resolve per theme.
The redesign had to survive six themes
Editorial Ink is two of six themes. The app also ships four paid skins that players have already bought. A redesign that only looked right in light and dark would break the thing people paid for.
Because every new token was defined for all six themes, the board’s typography, grid, selection, and framing render from the same code in every skin. The skin changes the values; the roles stay put. That is the practical payoff of semantic tokens: a visual overhaul becomes a change to a color table, not a fork in the renderer. It also means the next surface we move to Editorial Ink inherits skin support for free, instead of each screen re-deriving “what is the selection color in ocean?”.
One vocabulary for the app and the website
Tokens only pay off when there is exactly one set of names. Our marketing sites had their own dialect: ink, mist, line, and an accent scale that was really the brand. In the same cycle we renamed those to the canonical roles — accent became primary, ink became foreground, mist became background/surface, line became border, muted became muted-foreground. Same values, shared vocabulary. Now a sentence like “selection uses a tint of primary” means the same thing in the game and on the landing page.
That is why the board’s selection color is not a decoration decision made in isolation. It is a tint of primary at low alpha — a discreet purple that matches the website’s ::selection highlight at 30%. We tried a warm amber to echo the paper, reviewed it on a device, and dropped it: it fought the numbers instead of guiding the eye. The token that shipped is the purple tint, recorded as the single source of truth so nobody reinvents it next month.
Tokens that belong to the board
Not every visual need is a new semantic role. Three tokens are board-derived: they exist for the Sudoku surface and stay in the app, not in the shared contract.
boardLine: a variant ofborderfor the grid.boardShadow: the deboss shadow of the board frame.cellHighlight: the selection, related-cell, and same-number tint described above.
Keeping them named and derived, instead of scattered hex values inside the renderer, is what lets the same board read correctly across light, dark, and four paid skins without a separate code path per theme.
The board is where identity has to survive rendering
The board carries the game’s personality, and it renders two ways: a Skia canvas (the default in production) and a React Native fallback, in parity. Both got the same treatment:
- A typographic hierarchy between the fixed givens (heavier weight) and the player’s own guesses.
- Tabular figures, so the digits do not jitter as they change.
- A hairline grid with stronger 3×3 separators.
- Errors signaled by color and shape (a rounded border) and a small motion, never color alone. That keeps the “you made a mistake” feedback legible for color-blind players and above the 4.5:1 contrast floor.
- One rounded, debossed frame around the whole board instead of a shadow per cell.
That last choice is design and performance at once. A shadow per cell looks richer for a second and then wrecks the repaint budget on a weak phone. A single framed board with a boardShadow deboss gives depth without asking the GPU to composite 81 shadows on every selection.
The performance budget was the real ceiling
The cutoff device is a Redmi Note 7. Selecting a cell repaints the board, and that interaction is not allowed to regress. The latency probe ships off; we flip it on for the measurement and revert it, so production never pays for the instrumentation.
Cell selection with a full board repaint landed at a 107ms median, 141ms P90, and 143ms max across fifteen samples. No continuous input crossed the 180ms technical cutoff. The only spikes into the 250–330ms range came from the animated end-of-game and error flows, not from tapping cells. That is the honest shape of it: the everyday interaction stayed under budget, and the celebratory moments cost more but happen once.
Numbers like these are why the ambition is bounded. A prettier board that made selection feel slower would be a worse product, not a bolder one.
What we did not do yet
Calling this “the redesign” would be dishonest. It shipped as a vertical slice: the token foundation, the board, and the Home screen — a masthead, a resume-game card, a Daily Challenge card, and a loading skeleton instead of a raw spinner, with new i18n keys added across all ten languages.
The rest is deliberately still on the old look:
- End-of-game, the NumberPad and control bar, Profile, Shop, and Settings have not moved to Editorial Ink yet. They reuse the same tokens, so the remaining work is spreading a direction, not inventing one.
- A custom display typeface for the digits is deferred. It means a native rebuild and more bundle weight, so the slice uses a system font with a tabular weight until that trade is worth it.
- Right-to-left languages (Arabic, Urdu) are catalogued, and the slice does not introduce layout that breaks in RTL, but global mirroring is still a later task.
The lesson
A bold redesign in a mature app is mostly editing the system underneath the screens. Name the direction so every later decision has something to answer to. Move color into semantic tokens so the app and the website say the same words. Let the performance budget, not taste, set the ceiling. Then ship a slice you can defend, with an honest list of what is still on the old look.
That is less satisfying than announcing a finished redesign. It is also the version that is true, and the one that lets the next surface inherit the work instead of starting over.