Stop the colour group fighting the colour you are choosing

Fixkamo-internal
Shipped
August 23, 2026 at 3:04 AM UTC
Author
kamo
Commit
7229b37

ModernColorGroup was controlled and uncontrolled at the same time. It kept local state for every slot, mirrored the `value` prop into that state in one effect, and called `onChange` from another with a freshly built object whenever any slot moved. The parent stores what it is handed, so that new object identity re-triggered the mirror, and a setTimeout(..., 0) guard flag kept the cycle from ever converging. Two consequences. Mounted and untouched, the group never went quiet: it re-rendered the whole 3,400-line settings page for as long as the Branding editor was open. And every tick pushed the parent's copy back over the local state, so anything typed into the hex field was racing the mirror. It also derived the four shades from whatever the base field held mid-keystroke — "#c81" is a fine argument to parseInt, so no error surfaced and the shades were simply computed from a colour nobody had finished typing. Derive from `value`, call `onChange` only from real events, and refuse to derive shades from an incomplete hex. The state machine moves to colorGroupState.ts so it can be tested directly; the component keeps its markup. Three of the four new component tests fail against the old version, including the one that shows "#c81" publishing lighter: #1e2a9f.

All changes

Like what you see shipping?

Every one of these updates lands in your workspace automatically. Start free and watch it grow week after week.

Start Free ForeverView Pricing