honza.codes
← Back

Forgiving color inputs

Almost every color picker I've used wasn't built for power users. You get a clickable palette, maybe a hex field that expects exactly six characters. No shortcuts. But sometimes I just want to type f and get white. Or red and move on. Or 0 for black.

So I built expand-to-hex-color — a tiny utility that takes whatever you throw at it and expands it to a full hex value. CSS color names, single characters, shorthand notation. If it can reasonably be a color, it becomes one. And even when it can't — like Batman — it extracts the valid hex characters and does its best.


Type anything, get a color

Try white, f, red, or primary — watch the swatch update as you type.


The expansion logic

The idea is to figure out the most reasonable hex color from whatever the user typed. Here's the full set of rules:

InputOutputRule
white#FFFFFFCSS color name
f#FFFFFFSingle char repeated 6x
0#000000Single char → black
f0#F0F0F0Two chars repeated 3x
ABC#AABBCCShorthand hex (CSS-style)
ABCD#AABBCCDDShorthand with alpha
FF6600#FF6600Standard 6-char hex
Batman#BBAAAAExtracts valid hex chars (BAA → shorthand)

The order matters. Custom mappings are checked first, then CSS color names, then hex expansion. This means you can override built-in names — type red and get your brand's red instead of #FF0000. Handy.


Design tokens as color names

Most of us work in a context where we have our own brand colors, but color inputs have no idea about them. Pass a custom mapping and suddenly your team can type primary or danger and get the right hex. Custom mappings take priority over CSS names, so you can override anything:

const tokens = {
  primary: '#3498db',
  danger: '#FF0000',
  brand: '#6C5CE7',
  red: '#CC0000', // override the CSS built-in
}

expandToHexColor('primary', 'uppercase', true, tokens)
// → '#3498DB'

expandToHexColor('brand', 'uppercase', true, tokens)
// → '#6C5CE7'

// 'red' returns your brand red, not CSS #FF0000:
expandToHexColor('red', 'uppercase', true, tokens)
// → '#CC0000'

Wiring it into a React input

The expansion is synchronous and pure — just derive the color from input value on each render. No debouncing, no effects:

function ColorInput() {
  const [value, setValue] = useState('')
  const resolved = expandToHexColor(value)

  return (
    <div className="flex items-center gap-3">
      <input
        value={value}
        onChange={(e) => setValue(e.target.value)}
        placeholder='Try "white", "f", "red"'
      />
      <div
        className="h-9 w-9 rounded-md border"
        style={{ backgroundColor: resolved ?? 'transparent' }}
      />
      {resolved && <span>{resolved}</span>}
    </div>
  )
}

One function call per keystroke. That's it.


Get the package

npm install expand-to-hex-color

Works in any JS environment. The full API is one function:

import { expandToHexColor } from 'expand-to-hex-color'

expandToHexColor('f')       // → '#FFFFFF'
expandToHexColor('white')   // → '#FFFFFF'
expandToHexColor('0')       // → '#000000'
expandToHexColor('red')     // → '#FF0000'
expandToHexColor('ABC')     // → '#AABBCC'
expandToHexColor('nope')    // → null