Inside the BirthdayColor Algorithm: How Every Date Becomes a Color
A complete walkthrough of the JavaScript that decides which of 64 traditional Chinese colors gets attached to a given birthday — the actual code from the site, worked examples for a few real dates, and the reasoning behind each line.
The 30-second version
Two steps, both pure functions:
- Figure out which season the date falls in. Four buckets, decided by Chinese solar terms.
- Run the date through a small hash and take the result mod 16. That's your index into a 16-color pool for that season.
Total palette: 64 colors (16 × 4 seasons). Same date in, same color out. No randomness, no year argument, no server call. The whole thing lives in palette.js on the site.
Step 1 — From a date to a season
The site doesn't use Western quarter-year seasons. The boundaries follow the four "Beginnings" of the Chinese solar terms — 立春, 立夏, 立秋, 立冬 — which fall on roughly Feb 4, May 5, Aug 7, and Nov 7. In code, that's compressed into a small integer trick:
window.seasonOf = function(m, d){
const mmdd = m*100 + d;
if (mmdd >= 204 && mmdd <= 504) return "spring";
if (mmdd >= 505 && mmdd <= 806) return "summer";
if (mmdd >= 807 && mmdd <= 1106) return "autumn";
return "winter";
};
Reading that line by line:
m*100 + dpacks month and day into a single integer. March 8 becomes3*100 + 8 = 308. October 12 becomes1012. The trick lets us compare a whole date with one comparison instead of two.- Each
ifcovers one season as an inclusive range ofmmddvalues. - Winter wraps around year-end (Nov 7 through Feb 3), which is why it falls out as the default at the bottom rather than getting its own range.
| mmdd range | Season | Solar term boundary |
|---|---|---|
| 204 → 504 | Spring 春 | 立春 → 立夏 − 1 |
| 505 → 806 | Summer 夏 | 立夏 → 立秋 − 1 |
| 807 → 1106 | Autumn 秋 | 立秋 → 立冬 − 1 |
| 1107 → 1231 & 0101 → 0203 | Winter 冬 | 立冬 → 立春 − 1 |
The cutoffs are fixed across years. Real solar terms drift by roughly a day depending on the astronomical calendar, but a fixed table keeps the function pure: no year argument required, no leap-year edge case, no API call. Same date, same season, every year.
Step 2 — Picking a color inside the season
Once the season is known, the site has a fixed pool of 16 colors for it. The full code is just six lines:
window.colorOf = function(m, d){
const s = seasonOf(m, d);
const pool = PALETTE[s];
const hash = (m*31 + d*7 + m*d + 3*d*d) >>> 0;
return { season: s, ...pool[hash % pool.length] };
};
Everything interesting happens in the hash line:
const hash = (m*31 + d*7 + m*d + 3*d*d) >>> 0;
Four small terms, each doing a specific job:
m * 31— A pure month signal. 31 is prime, and bigger than any possible day, so adjacent months get cleanly separated buckets before the day even enters the picture.d * 7— A pure day signal. 7 is also prime, and small enough that day differences nudge the hash gently rather than dominating the result.m * d— A cross term. Without it, swappingmandd— say(3, 8)and(8, 3)— would change the hash by a purely linear amount. Multiplying them breaks that symmetry, so March 8 and August 3 don't drift toward the same index.3 * d * d— A quadratic day term, and the most important one. Without it, consecutive days like the 14th and the 15th tend to land on neighboring indices, which feels boring (you and a friend born one day apart would always get adjacent colors). Squaringdmakes the increment grow with the day, scattering close dates across the pool.
The >>> 0 at the end is a JavaScript-specific quirk — it coerces the result to an unsigned 32-bit integer. The arithmetic above only ever produces small positive numbers, so this is mostly defensive: it guarantees the modulo on the next line stays non-negative under any input.
Then:
hash % pool.length // pool.length is 16, always
…hands you a number between 0 and 15. That's your index into the season's color array. Done.
Worked examples
Let's run a few real birthdays through the function on paper.
March 8 — spring
mmdd = 308→ spring (since 204 ≤ 308 ≤ 504).hash = 3·31 + 8·7 + 3·8 + 3·64 = 93 + 56 + 24 + 192 = 365index = 365 mod 16 = 13
So March 8 picks up PALETTE.spring[13].
July 4 — summer
mmdd = 704→ summer.hash = 7·31 + 4·7 + 7·4 + 3·16 = 217 + 28 + 28 + 48 = 321index = 321 mod 16 = 1
December 25 — winter
mmdd = 1225→ winter (falls out as the default).hash = 12·31 + 25·7 + 12·25 + 3·625 = 372 + 175 + 300 + 1875 = 2722index = 2722 mod 16 = 2
February 29 — spring (leap-day birthday)
mmdd = 229→ spring.hash = 2·31 + 29·7 + 2·29 + 3·841 = 62 + 203 + 58 + 2523 = 2846index = 2846 mod 16 = 14
The site treats February as 29 days year-round, so leap-day babies always land on the same color.
Want to verify it yourself? Open the home page, open your browser's DevTools console, and run colorOf(3, 8) for any month/day. The function is exposed on window. There's no server call — the math is happening in your browser, on your CPU, in well under a millisecond.
The companion palette: a second use of the same hash
Each birthday color also comes with a 3-color companion palette — the small "colors that go with yours" set on the result page. Same season, same hash, but offset:
window.harmony = function(m, d){
const s = seasonOf(m, d);
const pool = PALETTE[s];
const hash = (m*31 + d*7 + m*d + 3*d*d) >>> 0;
const idx = hash % pool.length;
const offsets = [3, 7, 11];
return offsets.map(off =>
({ season: s, ...pool[(idx + off) % pool.length] })
);
};
The offsets [3, 7, 11] are intentionally uneven inside a 16-slot pool:
- +3 — a near neighbor that still feels distinct from your color.
- +7 — roughly halfway across the pool, the "complement" position.
- +11 — another irregular jump, so the three companions sit at indices that don't form a regular interval.
The four-color group (your color plus three companions) has an irregular, slightly off-balance rhythm — which is what designers usually want when they pull together a moodboard, instead of perfectly evenly-spaced hues that all cancel each other out.
Why a hash function and not a hand-curated table?
The original Japanese cafein.jp site does it the other way: a person sat down and assigned a specific color to each of 366 days by hand. There's a real charm to that approach — it's a personal artwork.
The hash approach trades editorial intimacy for three different qualities:
- Reproducibility. The function lives in
palette.js, runs in your browser, and produces the same result on any day in any year. There's no hidden lookup table. - Cultural depth per color. With only 64 colors in play, every one of them gets a fully-developed entry — a Chinese name, a poem citation, a personality reading, a hex value calibrated against historical samples. A 366-color hand-table would dilute that depth.
- A built-in "we" signal. Roughly five or six birthdays share each color. Some people find that disappointing at first — until they realize it's what makes phrases like "we're both Vermilion-East" possible.
How "fair" is the distribution?
A reasonable question: with this hash, are the 16 colors in each season equally likely? The honest answer is "close but not perfectly." Each season has roughly 92 days mapping into 16 buckets, so a perfectly uniform distribution would put about 5.75 days on each color. The actual counts come out close to that — within a small range — but not exactly equal.
I tested several variants of the hash function and picked one whose distribution looked close to uniform and whose calendar-neighbors usually landed on different colors. Those two goals pull against each other: maximizing one tends to weaken the other. The current function is a compromise that does both reasonably well.
Reading the source
If you'd like to inspect the live code, open palette.js. The relevant section is short — the four functions above (seasonOf, colorOf, harmony, plus a small daysInMonth helper) sit together in the file under the comment /* ---------- 核心函数 ---------- */. Everything else in palette.js is data: the 64 colors and their metadata.
If you want to argue with a design choice, fork it. The whole site is small enough to read in 30 minutes, and the math is small enough to redo in a notebook.
Where to go next
- For the cultural side — what each color actually means — see What Does Your Birthday Color Mean?.
- For the story behind why the site exists at all, see Why I Built BirthdayColor.
- For ways to actually use the result on a wall or in an outfit, see how to use your birthday color in your space, closet, and online.