50 Japanese Traditional Colors, Sorted By Hue — Because the Names Aren't Enough
The thing you need when browsing a color palette is adjacent colors that look adjacent. Every reference site I could find listed Japanese traditional colors alphabetically, which made scanning for the one I wanted unnecessarily painful. A five-minute switch to hue-order sorting made the same 50 colors feel like a different product.
I keep needing hex codes for Japanese traditional colors — 撫子色 (nadeshiko), 茜色 (akane), 瑠璃色 (ruri). Existing references are ad-heavy and sort by name, which means visually similar colors end up far apart on the page. I wanted a clean, hue-sorted reference where browsing feels like a color wheel.
🔗 Live demo: https://sen.ltd/portfolio/japanese-colors/
📦 GitHub: https://github.com/sen-ltd/japanese-colors
Each card shows the kanji name, its phonetic reading, and the hex code. Click to copy hex to clipboard. Filter by color family (reds / oranges / greens / etc.), or search by kanji, reading, or hex.
Vanilla JS, zero dependencies, no build. Color data is ~50 entries across ~80 lines, plus a handful of small helpers.
Hue-order sorting completely changes the UX
I initially listed colors in category declaration order: 紅 → 朱 → 茜 → 緋 → 紅梅 → 桜 → 撫子 → 珊瑚. Even within "reds", visually adjacent cards were often very different hues, so your eye had to constantly reset as it scanned. The grid felt noisy.
The fix was to convert every color to HSL and sort globally by hue:
export function hexToHsl(hex) {
const { r, g, b } = hexToRgb(hex)
const rn = r / 255, gn = g / 255, bn = b / 255
const max = Math.max(rn, gn, bn)
const min = Math.min(rn, gn, bn)
let h = 0, s = 0
const l = (max + min) / 2
if (max !== min) {
const d = max - min
s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
switch (max) {
case rn: h = (gn - bn) / d + (gn < bn ? 6 : 0); break
case gn: h = (bn - rn) / d + 2; break
case bn: h = (rn - gn) / d + 4; break
}
h /= 6
}
return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100) }
}
colors.sort((a, b) => hexToHsl(a.hex).h - hexToHsl(b.hex).h)
Now the grid walks around the color wheel — red → orange → yellow → green → blue → purple → back toward red — and the difference in browsing feel is dramatic. You stop actively searching; you start sweeping. The ordering reads as natural because human vision uses hue as its primary cue.
It's a nice reminder that for a visual browser, sort-key choice is UX. No amount of typography or spacing fixes a bad order.
Flipping label color by perceived brightness
Each card overlays the color name on a background tinted with the hex. Some colors are very light (桜色 #FEF4F4) and some are very dark (漆黒 #0D0015), so a single label color fails on one extreme or the other.
I flip between dark and light labels using a simple luminance check:
export function isLight(hex) {
const { r, g, b } = hexToRgb(hex)
const lum = 0.299 * r + 0.587 * g + 0.114 * b
return lum > 150
}
The 0.299 / 0.587 / 0.114 coefficients are ITU-R BT.601 luma weights, approximating how bright each RGB channel feels rather than how much light it emits. Green dominates; blue barely contributes. It's a pre-gamma-correction formula — WCAG's sRGB-accurate relative luminance is more correct — but for a 50-color palette where I just need to pick "light" vs "dark" labels, BT.601 is perfectly adequate and a lot shorter.
The 150 threshold is empirical: I tuned it so that the soft pastels (桜色, 生成り) count as "light" and switch to dark text.
Search that accepts kanji, reading, or hex
The search box needs to accept several input types because users have different ways of remembering a color:
- Kanji (
紅) - Reading in hiragana (
くれない) - Hex (
B90016) - English category name (
red)
A single substring check across all of those fields handles it:
function matches(color, query) {
const q = query.toLowerCase().replace(/\s/g, '')
if (!q) return true
return (
color.name.includes(q) ||
color.reading.includes(q) ||
color.hex.toLowerCase().includes(q) ||
color.category.toLowerCase().includes(q)
)
}
The reading field intentionally stores hiragana with no spaces, so なでしこ matches なでしこいろ without the user having to type the suffix. Small detail, makes "I only remember part of the name" forgiving.
Copy-to-clipboard with a fallback
Clicking a card copies its hex to clipboard so you can immediately paste into Figma or your editor. navigator.clipboard.writeText is the modern API, but it can be blocked by the Permissions API, so I keep an old-school fallback:
card.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(color.hex)
showToast(`Copied: ${color.hex}`)
} catch {
const ta = document.createElement('textarea')
ta.value = color.hex
document.body.appendChild(ta)
ta.select()
document.execCommand('copy')
document.body.removeChild(ta)
showToast(`Copied: ${color.hex}`)
}
})
document.execCommand('copy') is formally deprecated but still ships in every browser, and it works without permission prompts. Keep it as a fallback until it actually stops working.
Testing the data, not the code
The most useful tests for a data-driven tool like this are checks on the data itself. Entering 50 colors by hand means typos are inevitable, and the easy-to-miss kind are: wrong hex length, unknown category id, empty name field:
test('all colors have valid 6-digit hex', () => {
for (const c of COLORS) {
assert.match(c.hex, /^#[0-9A-Fa-f]{6}$/)
}
})
test('all colors have non-empty name and reading', () => {
for (const c of COLORS) {
assert.ok(c.name.length > 0)
assert.ok(c.reading.length > 0)
}
})
test('all categories are known', () => {
const validIds = new Set(CATEGORIES.map((c) => c.id))
for (const c of COLORS) {
assert.ok(validIds.has(c.category), `unknown category: ${c.category}`)
}
})
Now adding a new color that references a typo'd category id fails CI immediately. The lesson I keep relearning: when the source of truth is the data, test the data.
Tests
11 cases on node --test, covering data integrity plus:
-
isLightat boundaries (pure white, pure black, 50% gray) -
hexToRgbround-trip -
hexToHslon reference hues (red = 0, green = 120, blue = 240)
npm test
Series
This is entry #8 in my 100+ public portfolio series.
- 📦 Repo: https://github.com/sen-ltd/japanese-colors
- 🌐 Live: https://sen.ltd/portfolio/japanese-colors/
- 🏢 Company: https://sen.ltd/
If you spot a missing color or a wrong reading, issues welcome.

Top comments (0)