DEV Community

Byte Vancer
Byte Vancer

Posted on

How to Generate Random Colors in JavaScript (Without the Muddy Results)

Generating a random color sounds trivial — and the one-liner everyone reaches for
is trivial. The problem is it produces a lot of muddy, low-contrast, unusable colors.
Let's start with the classic trick, see why it disappoints, and then generate colors
that actually look good (and stay readable). Code is plain JS you can paste into a
console.

The classic one-liner (and its bug)

const randomHex = () =>
  "#" + Math.floor(Math.random() * 0xffffff).toString(16).padStart(6, "0");

randomHex(); // "#3a9f21"
Enter fullscreen mode Exit fullscreen mode

One thing people forget: padStart(6, "0"). Without it, a small number like
0x0042ff becomes "42ff" — a 4-character string that's an invalid color. That's the
source of the occasional "why is my random color broken?" bug. Always pad to 6 hex
digits.

But even when it's correct, this approach has a deeper issue.

Why uniform RGB looks muddy

Math.random() * 0xffffff picks a point uniformly in RGB space. The trouble is that
most of RGB space is unattractive — desaturated browns, grays, and muddy
mid-tones. You'll get the occasional nice color, but also a steady stream of sludge,
and colors with wildly different brightness, so a set of them has no visual harmony.

If you just need a color and don't care how it looks, uniform RGB is fine. If you're
generating colors a human will see — avatars, tags, charts, backgrounds — you want
more control. That's where HSL comes in.

Generate pleasant colors with HSL

HSL (hue, saturation, lightness) lets you randomize the hue (which color) while
pinning saturation and lightness to ranges that always look good:

function randomPleasantColor() {
  const h = Math.floor(Math.random() * 360);   // any hue
  const s = 60 + Math.random() * 20;           // 60–80% — vivid, not neon
  const l = 45 + Math.random() * 10;           // 45–55% — mid, readable
  return `hsl(${h} ${s.toFixed(0)}% ${l.toFixed(0)}%)`;
}

randomPleasantColor(); // "hsl(214 72% 51%)"
Enter fullscreen mode Exit fullscreen mode

Because saturation and lightness stay in a tasteful band, every color this returns
is usable. Want pastels? Push lightness up (80–90%) and saturation down. Want dark
mode accents? Drop lightness to 30–40%.

Generating a harmonious palette

For multiple colors that belong together, don't call the function N times — spread the
hues evenly around the wheel from a random starting point:

function randomPalette(n) {
  const start = Math.random() * 360;
  return Array.from({ length: n }, (_, i) => {
    const h = Math.round((start + (i * 360) / n) % 360);
    return `hsl(${h} 65% 55%)`;
  });
}

randomPalette(5);
// ["hsl(291 65% 55%)", "hsl(3 65% 55%)", "hsl(75 65% 55%)", ...]
Enter fullscreen mode Exit fullscreen mode

For a more organic (less "evenly sliced") feel, step the hue by the golden angle
(~137.5°), which distributes hues so they rarely collide — the same trick nature uses
for leaf arrangement:

const GOLDEN_ANGLE = 137.508;
function goldenPalette(n) {
  let h = Math.random() * 360;
  return Array.from({ length: n }, () => {
    h = (h + GOLDEN_ANGLE) % 360;
    return `hsl(${h.toFixed(1)} 65% 55%)`;
  });
}
Enter fullscreen mode Exit fullscreen mode

Keep it readable: pick contrasting text

If you're putting text on a random background (tags, avatars), compute the background's
relative luminance and choose black or white text accordingly:

function readableTextColor({ r, g, b }) {
  // sRGB relative luminance (WCAG)
  const lin = (c) => {
    c /= 255;
    return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
  };
  const L = 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
  return L > 0.179 ? "#000000" : "#ffffff";
}
Enter fullscreen mode Exit fullscreen mode

This is the difference between an avatar you can read and one where the initials vanish.
For anything user-facing, check your color pairs against WCAG AA (a contrast ratio of at
least 4.5:1 for normal text).

Bonus: reproducible "random" colors

Sometimes you want the same user to always get the same color (consistent avatars,
chart series). Seed a small PRNG with a hash of their id instead of Math.random():

function mulberry32(seed) {
  return function () {
    seed |= 0; seed = (seed + 0x6D2B79F5) | 0;
    let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

function colorForId(id) {
  let hash = 0;
  for (const ch of String(id)) hash = (hash * 31 + ch.charCodeAt(0)) | 0;
  const h = Math.floor(mulberry32(hash)() * 360);
  return `hsl(${h} 65% 55%)`;
}

colorForId("user-42"); // always the same hue for "user-42"
Enter fullscreen mode Exit fullscreen mode

When you just want the color, not the code

If you're picking a color by hand — for a mockup, a CSS variable, a quick test — you
don't need to write any of this. I maintain a free, browser-based
Random Color Generator
(a random colour generator if you're on the British-spelling side of the internet)
that spits out random colors with HEX, RGB and HSL values ready to copy, and lets you
generate a fresh set on demand. It runs entirely client-side and needs no signup — part
of ByteTools, a set of free color and CSS tools
(palette generators, contrast checkers, gradient makers, and format converters).

TL;DR

  • The # + random hex one-liner works, but pad to 6 digits and expect muddy output.
  • Randomize hue in HSL with pinned saturation/lightness for colors that always look good.
  • Spread hues evenly (or by the golden angle) for harmonious palettes.
  • Compute luminance to pick readable black/white text, and check WCAG contrast.
  • Seed a PRNG when you need the same color for the same id.
  • Or skip the code and grab one from a free random color generator.

What's your go-to for random colors — uniform RGB, HSL, or something fancier? Let me
know in the comments.

Top comments (0)