DEV Community

Cover image for Building a Color Spin Wheel: 4 Small Fixes That Made It Feel Finished
Devid E
Devid E

Posted on

Building a Color Spin Wheel: 4 Small Fixes That Made It Feel Finished

I added a "Wheel of Colors" page to my NameWheelPro project. You add colors, spin, and get a random one. The whole thing is vanilla JS with zero dependencies, so every fix below works in any project.

If you're building your own color spin wheel, here are four small problems worth solving early, with the code.

  1. #F80 and #ff8800 are the same color

Users paste colors in every format, so a plain string comparison misses duplicates. Normalize before you compare:

function normalizeHex(hex) {
  let h = hex.replace('#', '').toLowerCase();
  if (h.length === 3) h = [...h].map((c) => c + c).join(''); // f80 -> ff8800
  return '#' + h;
}

function addColor(list, name, hex) {
  const key = normalizeHex(hex);
  if (list.some((c) => normalizeHex(c.hex) === key)) return list; // already there
  return [...list, { name, hex: key }];
}
Enter fullscreen mode Exit fullscreen mode

Deduping by hex, not by name, also stops "Red" and "Crimson" from being two identical slices.

  1. Share links should stay short

A shareable wheel needs no backend if you put the colors in the URL. The trick is a compact format, so 16 colors don't become a 1,000-character link:

function buildShareUrl(colors) {
  const payload = colors
    .map((c) => `${encodeURIComponent(c.name)}:${c.hex.slice(1)}`)
    .join(',');
  return `${location.origin}${location.pathname}?c=${payload}`;
}

function readShareUrl() {
  const raw = new URLSearchParams(location.search).get('c');
  if (!raw) return null;
  try {
    return raw.split(',').slice(0, 100).map((part) => {
      const [name, hex] = part.split(':');
      return /^[0-9a-f]{6}$/i.test(hex)
        ? { name: decodeURIComponent(name), hex: '#' + hex }
        : null;
    }).filter(Boolean);
  } catch {
    return null; // malformed link, fall back to defaults
  }
}
Enter fullscreen mode Exit fullscreen mode

I use , and : as delimiters because encodeURIComponent always escapes them inside names. The hex check and the slice(0, 100) cap matter too, since data from a URL should never be trusted.

  1. Don't rely on color alone

A color picker has an obvious accessibility trap: the result is only a color. For color-blind users, or anyone using a screen reader, that isn't enough. Always show and announce the name and hex:

function announce(color) {
  const live = document.getElementById('result-live'); // <div aria-live="polite">
  live.textContent = `Result: ${color.name}, ${color.hex}`;
}
Enter fullscreen mode Exit fullscreen mode

The swatch is the fun part, but the text is what makes the result usable for everyone.

  1. History that survives a refresh

The History tab keeps recent results. Capping it and wrapping storage in try/catch keeps it from ever breaking the app:

function saveHistory(history, color) {
  const next = [{ name: color.name, hex: color.hex, at: Date.now() }, ...history].slice(0, 30);
  try {
    localStorage.setItem('colorwheel:history', JSON.stringify(next));
  } catch { /* storage full or blocked: keep working in memory */ }
  return next;
}
Enter fullscreen mode Exit fullscreen mode

Storage can be full, disabled, or blocked in private mode. The wheel should still spin.

Wrapping up

None of these are hard. Together they separate a demo from a tool people trust: normalized data, compact sharing, accessible results, and safe persistence.

You can try the live color spin wheel here, with no signup. What small detail made your last side project feel finished?

Top comments (0)