NNicheStackTutorials
← All tutorials
Frontend Tools

hexrgb: Design Handoff Colors Without Opening Figma

Freelance landing page needed hex ↔ rgb ↔ hsl with live preview. One input field, three outputs, clipboard copy — 75 lines of JS.

·9 min read·Frontend Tools
hexrgb: Design Handoff Colors Without Opening Figma cover

Problem: Client sent #0ea5e9 in Slack and rgb(14, 165, 233) in the style guide. Built a converter instead of alt-tabbing.

Full tool

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Color Converter</title>
  <style>
    body { font-family: system-ui; max-width: 520px; margin: 2rem auto; padding: 0 1rem; }
    .swatch { width: 100%; height: 80px; border-radius: 8px; border: 1px solid #ccc; margin: 1rem 0; }
    input[type=color] { width: 48px; height: 48px; border: none; cursor: pointer; }
    .field { display: flex; gap: .5rem; align-items: center; margin: .5rem 0; }
    .field input[type=text] { flex: 1; font-family: monospace; padding: .4rem; }
    button { padding: .35rem .6rem; cursor: pointer; }
  </style>
</head>
<body>
  <h1>Hex / RGB / HSL</h1>
  <input type="color" id="picker" value="#0ea5e9">
  <div class="swatch" id="swatch"></div>
  <div class="field"><label>HEX</label><input id="hex" value="#0ea5e9"><button data-copy="hex">Copy</button></div>
  <div class="field"><label>RGB</label><input id="rgb" readonly><button data-copy="rgb">Copy</button></div>
  <div class="field"><label>HSL</label><input id="hsl" readonly><button data-copy="hsl">Copy</button></div>
  <script>
    const picker = document.getElementById('picker');
    const hexIn = document.getElementById('hex');
    const rgbOut = document.getElementById('rgb');
    const hslOut = document.getElementById('hsl');
    const swatch = document.getElementById('swatch');

    function clamp(n, min, max) { return Math.min(max, Math.max(min, n)); }

    function hexToRgb(hex) {
      let h = hex.replace('#', '').trim();
      if (h.length === 3) h = h.split('').map(c => c + c).join('');
      if (!/^[0-9a-fA-F]{6}$/.test(h)) return null;
      return { r: parseInt(h.slice(0,2),16), g: parseInt(h.slice(2,4),16), b: parseInt(h.slice(4,6),16) };
    }

    function rgbToHsl(r, g, b) {
      r /= 255; g /= 255; b /= 255;
      const max = Math.max(r,g,b), min = Math.min(r,g,b);
      let h = 0, s = 0, 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 r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
          case g: h = ((b - r) / d + 2) / 6; break;
          case b: h = ((r - g) / d + 4) / 6; break;
        }
      }
      return { h: Math.round(h*360), s: Math.round(s*100), l: Math.round(l*100) };
    }

    function updateFromRgb(r, g, b) {
      const hex = '#' + [r,g,b].map(x => x.toString(16).padStart(2,'0')).join('');
      const hsl = rgbToHsl(r, g, b);
      hexIn.value = hex;
      picker.value = hex;
      rgbOut.value = 'rgb(' + r + ', ' + g + ', ' + b + ')';
      hslOut.value = 'hsl(' + hsl.h + ', ' + hsl.s + '%, ' + hsl.l + '%)';
      swatch.style.background = hex;
    }

    picker.oninput = () => updateFromRgb(...Object.values(hexToRgb(picker.value)));

    hexIn.oninput = () => {
      const rgb = hexToRgb(hexIn.value);
      if (rgb) updateFromRgb(rgb.r, rgb.g, rgb.b);
    };

    document.querySelectorAll('[data-copy]').forEach(btn => {
      btn.onclick = () => {
        const id = btn.dataset.copy;
        navigator.clipboard.writeText(document.getElementById(id).value);
      };
    });

    updateFromRgb(14, 165, 233);
  </script>
</body>
</html>

Invalid hex silent fail

Typing #gggggg used to blank the swatch. Now hexToRgb returns null and we skip update until valid.

Alpha channel

For rgba(), extend with optional opacity slider — out of scope for v1 but clients rarely need it on static landers.

Verify

Picker drag → all three fields sync Paste #fff → expands logic via 3-char shorthand Copy RGB → pastes into Tailwind arbitrary value [rgb(14,165,233)]

Tags: ColorCSSVanilla JSDesign