Back

We rebuilt our sticky note colors like a pack of Post-its

Something about our toolbar felt disjointed. Six color swatches sat in a rail on the side of every board, and I could not tell you what was wrong with them. They just looked like six strangers who’d been told to wait in the same line.

So I did what any reasonable person does when they can’t put a feeling about six squares into words. I asked Claude to critique the toolbar as David Hussman.

David was an agile coach who spent his career asking teams why a thing existed before asking how to build it. That question works on anything, it turns out, including colored squares. The squares did not have a good answer.

The debate that followed took in Miro’s forums, FigJam’s forums, an argument with Material Design, a color-blindness simulator, and more time on the Post-it website than I’d like to admit. It ended somewhere I didn’t expect. The toolbar got smaller: five swatches, down from six. The drawer behind it got bigger: eight hues, each in three weights. And both are organized the way 3M has always sold sticky notes, in packs.

Also, two of our card colors turned out to be the same color. You’d think someone would have noticed that.

Colliding pairs before
13Of 120, below the threshold where two card-sized areas read as different colors.
Tightest pair
3.7blue and skyblue. Four degrees of hue apart.
Toolbar minimum now
20.6Was 7.4, and that pair sat side by side in the toolbar.
Places the palette lived
5Four hand-synced copies plus one orphan that called itself canonical.

Disjointed is a feeling. ΔE is a number.

A feeling about six squares is the kind of thing you can argue about forever, so the first move was to stop arguing and measure. ΔE2000 is the standard perceptual distance between two colors. Roughly, ΔE 1 is a just-noticeable difference in ideal lab conditions. For flat card-sized areas viewed side by side, you want 15 or more before people reliably name two colors as different: across a room, through a projector, on a cheap monitor.

Of the 120 pairs in our sixteen-color palette, thirteen came in under 15. Three were severe. Here they are butted edge to edge. That’s the honest test, because it’s what your eye does when two cards sit next to each other on a board.

blue / skyblueΔE 3.7
green / cyanΔE 5.9
orange / redΔE 7.4
pink / purpleΔE 10.4
white / blueΔE 11.1
yellow / mustardΔE 13.0

blue and skyblue differed by four degrees of hue and three points of lightness. That isn’t a second color. It’s a rounding error with a name.

The orange/red pair at ΔE 7.4 was the expensive one. Those two sat next to each other in the six-swatch toolbar, the colors people reach for most. Six pastels from a 13-point lightness band, two of them nearly identical. That was the disjointed feeling.

What Miro and FigJam users actually complain about

Before picking anything, we read what users of the two biggest whiteboard tools say about their palettes. It is not “give us prettier colors.” It’s three things, and each one ended up deciding something.

They run out. A long-running Miro request asks simply for more colors, because people mapping several kinds of information onto one board use up the set. One commenter adds that accommodating color-blind colleagues shrinks the usable set further. That turned out to be the most important sentence we read all week.

Paler reads as worse. Figma lightened FigJam’s stickies, reportedly for text contrast, and got a backlash: pale, washed out, low-contrast. Miro went the other way in its 2024 design-language update, raising vibrancy and contrast against the board. Two competitors ran the same experiment in opposite directions, and users preferred more saturation. Our pastels were worse off than either. Three of them sat at 1.00, 1.08 and 1.15 contrast against a white canvas. A contrast of 1.00 isn’t low contrast. It’s none.

Everyone wants custom colors. Both tools have long-running requests for them, including one from Miro users with protanopia who can’t tell the defaults apart. Figma’s answer was custom palettes on its Organization and Enterprise plans: a constrained set, applied broadly, instead of per-sticky freedom. That distinction shaped what we did next.

Material Design: right mechanism, wrong shape

Material 3 doesn’t publish a sticky-note palette. It publishes a generator, and the mechanism is genuinely useful.

It works in HCT (hue, chroma, tone), a space built so that tone maps to measured perceptual lightness. That’s the whole point: you can change a color’s hue and chroma without changing its tone. From a key color it derives a tonal palette of 13 tones numbered 0 to 100, and contrast becomes arithmetic instead of judgment. A fixed tone distance guarantees a contrast ratio regardless of hue.

Container roles sit at tone 90 in light theme. Tone 90 is precisely the pastel sticky-note register. Material had, in effect, already specified our pastel pack: eight hues, chroma held constant, all pinned to one tone.

So we built it to check. Eight hues at fixed lightness, textbook tone 90:

A textbook constant-tone row: 14.8:1 text contrast, and ΔE 0.4 under deuteranopia

Text contrast is a uniform 14.8:1 across all eight. Genuinely excellent, exactly as advertised. Then we ran it through simulated red-green color blindness, and the minimum separation collapsed to ΔE 0.4. Purple, blue and teal become one color. The text on all three is still very easy to read.

Material’s tonal system is right for UI surfaces, where colors are never asked to be a legend. It is wrong for categorical encoding, which is exactly what sticky notes are. Constant tone is the feature and the failure.

The reason is structural, and it governed every decision after this. Red-green color blindness collapses two of the three color channels, so hue alone cannot carry a distinction. Only lightness and blue-yellow survive. A row of colors tuned to identical lightness therefore cannot be made color-blind-safe at any hue. That’s a theorem, not a tuning problem.

That put a number on the Miro commenter’s aside. Lightness spread predicts color-blind survival, with one instructive exception:

PalettenL* spreadMin ΔE, deuteranopia
Ours, before161001.2
FigJam10212.8
Miro12364.0
Okabe–Ito (the color-blind-safe reference)88911.5

The exception is us. We had the widest spread of anyone and still came last, because all of that spread came from black and white at the two ends. The pastels in the middle were packed together. Nobody’s sticky-note palette comes close to Okabe–Ito, because pastel paper is by definition all one lightness. But Miro’s wider spread is measurably why it holds up better than FigJam’s.

You don’t configure a pack of Post-its. You buy one.

The organizing idea came from the physical object. Different packs of sticky notes at the store are different registers. A pack shares tone and chroma and varies only in hue. That’s a coherent design object, because sameness of tone is what the eye reads as “same material.” Post-it’s own product line has always worked this way: Beachside Café, Playful Primaries, Poptimistic, Supernova Neons. Each is a register with a hue rotation inside it. (3M’s naming department is clearly having more fun than ours.)

It also named what was wrong with our sixteen. Nine were pastels crammed into a 13-point lightness band. Three were saturated mid-tones. One (mustard) belonged to neither. That wasn’t one palette. It was two registers shuffled into one list, plus an orphan.

The first version of this made “pack” a per-board setting. That was wrong for the same reason the analogy is right: a setting turns a purchase into a decision, and every board would face that decision before it had a single card on it. So the pack isn’t a mode. It’s what’s on the toolbar, with the rest one click behind it.

Two registers or three

Convention says two. Notion offers each color twice. FigJam and Miro are effectively single-register. Nobody in this category ships three. And two registers get color blindness to ΔE 9.5, against three registers’ 10.9. That’s most of the benefit for two thirds of the tray.

We shipped three anyway, because of one map type where color isn’t a preference at all.

The most important card on the wall was invisible

Three weeks before this, we shipped Event Storming, the first map type whose notation is chromatic. On an Event Storming board the sticky colors aren’t a preference. They’re a published grammar the domain-driven-design community already reads. Color is the meaning.

Measured against the old palette, that notation’s minimum separation was ΔE 7.4, and the worst pair was the one that mattered most:

domain_event / hotspotΔE 7.4
policy / external_systemΔE 10.4
aggregate / actorΔE 13.0

hotspot is the marked disagreement, the thing our own code calls “the workshop’s real output.” It was ΔE 7.4 from domain_event, the most common card on the wall. On a projector, the output of the workshop was invisible against its own background.

We had built a map type for surfacing disagreement, then colored the disagreement to match the wallpaper.

This is what the third register bought. Because the notation is a projection and not a user’s choice, it can be specified across registers. That’s what takes it from broken to genuinely color-blind-safe, not just prettier:

Event Storming notationBeforeAfter
Minimum separation, normal vision7.423.6
Minimum under deuteranopia1.610.9
Minimum under protanopianot measured12.7
After: the notation spans all three registers, which is what makes it survive simulation
domain event#ff993f
command#ccdfff
aggregate#f8dc90
actor#007275
policy#cd9cff
read model#00783a
external system#ff8abc
hotspot#ba0c09

Eight hues, three registers, five on the rail

So the drawer holds eight hues in three registers. Every value was generated in OKLCH, with hue angles chosen to maximize the separation within each register.

Before: 16 in a flat list, 13 colliding pairs
After: 8 hues × 3 registers, bold row ΔE 18.7

The rail carries five, not six, because of a color that was already there: a card with no color set renders white. Five swatches means six card appearances on a board, which is where the retail packs land (Playful Primaries is four pads, Beachside Café five). They run in rainbow order, descending from yellow, so the rail reads as a spectrum instead of an inventory.

The rail: five, descending hue, plus the white card you start with
yellow#fac700
red#ff9384
purple#cd9cff
blue#8ab7ff
green#39d678
whiteno color set

Then we put it on a real board

Every number above passed. Two things on a real board still looked wrong, which says something about the numbers.

Bold yellow looked brown

It wasn’t a picking mistake. It was the gamut. Yellow’s chroma peaks near OKLCH lightness 0.87. Held at the bold register’s 0.775, it had nowhere to go but olive. Nobody puts olive on a toolbar on purpose. So yellow became the register’s one documented exception, lifted as high as it can go before the deep yellow beneath it stops clearing the floor.

Before: #d6b300, held at the register's lightness
yellow#d6b300 · ΔE 7.3 from brand
orangeits neighbor
After: #fac700, lifted out of the register
yellow#fac700 · ΔE 4.8 from brand
orangeΔE 20.9 apart

Our brand yellow was the obvious fix, so we tried it. It quietly dropped the deep row to ΔE 12.3, and nothing failed, because we’d only ever tested the toolbar and the notation. The shipped value lands ΔE 4.8 from the brand color anyway.

A grey too close to the board

stone was a grey whose main quality was being almost the background: ΔE 5.6 from the default board canvas and 6.1 from white. That’s the exact defect this work existed to remove, hiding in the neutrals. We cut it.

Five copies, and nobody could say why

The palette lived in five places: four hand-synced copies across Ruby, the stylesheets and JavaScript, plus an orphaned set of tokens describing itself as canonical, with zero consumers. It was the only copy nobody used, and the only one that claimed to be in charge.

None of that caused a collision. It was just a mess. But this is where the Hussman question landed hardest. Why did five copies exist? Nobody could say. So most of them stopped existing, and the change removed more code than it added. Two declarations remain, because the stylesheet compiler can’t read Ruby, and a test fails if they ever disagree.

Every failure became a test

The orange/red pair on the toolbar became a separation floor for the toolbar. The invisible hotspot became a check on the Event Storming notation, run again under simulated color blindness. stone became a rule that no colored fill sits within ΔE 10 of the board canvas. And bold yellow became a floor for every register, not just the ones we happened to be looking at.

The notation check alone would have caught hotspot with a number instead of someone squinting at a projector. Together, they’re a better answer to “what’s wrong with the toolbar” than the one I started with.


Measurements: sRGB → CIELAB with ΔE2000 for perceptual distance, WCAG relative luminance for contrast, and Machado et al. transfer matrices for color-vision simulation. Palette values generated in OKLCH with per-hue sRGB gamut clamping. Numbers in this post are the ones the test suite asserts.