Moving one brand color across app, web and mini program
Three platforms, ten color roles each, and three lists that barely overlap. That is not a naming convention, it is three different sets of constraints. Every number below is computed from the 100 palettes in this library, and none of it would hold for another one.
Ten roles, three lists that barely overlap
Put the three role lists side by side and the intersection is two concepts: background and primary. The other eight are named differently on each platform.
| Purpose | App | Web | Mini program |
|---|---|---|---|
| Primary | primary | primary | theme |
| Darker / interaction | primaryDark | primaryHover + primaryActive | btn |
| Lighter version | primaryLight | none | tag |
| Secondary | secondary | none | none |
| Accent | accent | accent + link | price |
| Text levels | text (one level) | textMain + textSub | textMain + textSub |
| Divider | none | border | divider |
| Container | none | surface | card |
| Status | success / warning / danger | none | none |
| Disabled | none | none | btnDisabled |
That is 10, 10 and 10 roles. Same count, different sets. The practical consequence: carry a web token set into a mini program and three of its keys do nothing, because success, warning and danger have no counterpart there, while three keys the mini program actually needs (price, tag, divider) are missing. Go the other way and web is short a border and a second text level.
So "one brand color" is true only of the primary. The primary can stay; the other eight roles have to be rewritten per platform.
Interaction: web darkens along one line, app spreads to both sides
Web has two interaction states. App has none of them, and the reason is the input device rather than the screen. A mouse pointer can hover. A finger has no middle state between pressing and lifting.
Measured across the web palettes here: primaryHover sits 13.9 lightness points below the primary (range 5.3 to 14.2), and primaryActive sits 26.1 points below. The second is about 1.9 times the first.
The app does something else. It gives the primary two flanks: primaryDark is 14.0 points darker, primaryLight is 32.0 points lighter. The rise on the light side is 2.3 times the drop on the dark side.
Why the light side has to go further: the dark variant only ever marks a press, while the light one gets used as a tag background and as the fill of a selected area. Those are large surfaces, and an insufficiently light fill leaves no contrast headroom for dark text sitting on it. The dark side can move a little. The light side cannot.
Working numbers: darken hover by 10 to 14 lightness points, then take active to roughly twice that. Narrower and the press is invisible. Wider and one tap looks like a different brand.
The mini program calls its accent price, and that is not sloppiness
Web and app both name their accent accent, and it is decorative: badges, highlights, things that should be noticed without carrying information. The mini program has no accent. It has price.
The difference is what the color is for. In a mini program the accent serves a transaction, and price is its only job. Measured here, price sits at a median 4.50:1 against the background, with a low of 3.65:1, every one of them above the body text threshold. That is not a coincidence. Price is information that must be read, not decoration, so it is held to the text standard instead of the decorative one.
Which generalizes into a rule worth keeping: whether a color needs to clear a contrast threshold depends on whether it carries information or atmosphere. A decorative accent may sit low. An accent carrying a price, a status or a count may not. Same hue position, two uses, two standards.
Web divides with lines, the mini program stacks cards, the app signals state
Each platform drops what it does not need, and the omissions line up with how it organizes a screen.
- Web has
surfaceandborder, and no status colors. Long pages divide with hairlines and faint fills, and a form can run two thousand pixels tall with a rule telling you something changed. - The mini program has
card,dividerandtag, also no status colors. The page is a card stack, one card per unit of information, and the gaps between cards do most of the dividing. - The app has
success,warninganddanger, and neithersurfacenorborder. Actions on a phone have consequences, a delete or a submit cannot be taken back, so state needs to be explicit.
Which settles the first question when porting: do not move colors first, look at what establishes hierarchy on the target. Carry web colors into a mini program without its spacing and the cards collapse into a smear. Carry app colors to the web and the status roles have nowhere to live, ending up as three tokens nobody calls.
The exception: in a dark theme, primaryLight can be inverted
Above, primaryLight sits 32.0 points lighter than the primary. That is the median. One palette in the library goes the other way: a primary of #4c8df6 at lightness 63, and a primaryLight of #1e3a63 at lightness 25, darker by 38 points.
That palette is a dark theme. Some dark implementations read primaryLight as "the tinted container for the primary" and hand back a darkened surface instead of a lightened one. The name did not change. The value did.
The failure is concrete: treat primaryLight as a light fill by name and this palette gives you dark navy behind dark text, contrast collapsing to unreadable.
The rule is one line: measure before you trust the name. Compare the lightness of primaryLight against the primary. Lighter, and it is a fill. Darker, and in this palette it is a deep container that needs light text on it. The same check applies to any palette you import and any design system you inherit.
Building one platform? You need four roles, not thirty
The above covers the case where all three platforms ship. Most projects ship one, or ship one and expand later, and then there is no reason to build thirty roles. Each platform has a minimum set, and four roles carry a page.
| Platform | Minimum set | What breaks first without it |
|---|---|---|
| Web | primary + bg + textMain + border | No border and tables and forms lose their structure, leaving spacing to do all the work |
| Mini program | theme + bg + textMain + card | No card and the card stack flattens into one long column of text |
| App | primary + bg + text + primaryDark | No primaryDark and pressing a button produces no feedback at all |
Four roles get a usable interface out the door, with three gaps: no secondary text, so supporting details have to share the body color; no accent, so anything that needs attention borrows the primary; and no disabled state, so an unavailable button looks exactly like an available one. None of that blocks a launch. All of it shows up afterward, one "why does this look off" at a time.
So the sequence is: ship with four, then add what the interface actually asks for, in this order. Secondary text, then disabled states, then accent. The first affects legibility, the last affects looks, and the middle one decides whether people think a button can be pressed.
What to change, in order
- Leave the primary alone. Brand recognition depends on it and it must match across platforms.
- Web adds
primaryHover(10 to 14 points darker),primaryActive(about twice that), plusborder,surfaceandtextSub. No status colors. - Mini program replaces web's
accentwithpriceand holds it to the text standard, then addscard,divider,tagandbtnDisabled. - App adds
primaryDark(14 points darker),primaryLight(32 points lighter, and genuinely lighter),secondaryand the three status colors. Nosurface, noborder. - Dark themes get one extra check: is
primaryLightactually lighter than the primary.
The supporting data is all filterable: the accessibility overview lists the whole library by contrast, the editorial standard explains where the thresholds come from, and each platform page shows the measured values for one palette.