<ColorPicker />

React component with fully controlled leaves. Essential state centralised.

by mcsf

HTML

<div id="app"></div>

CSS

*:focus {
  outline: #f66 dashed;
}

#app {
  padding: 1em;
  max-width: 20em;
  margin: auto;
  font-family: sans-serif;
}

.color-picker {
  display: flex;
  justify-content: center;
  flex-direction: column;
  text-align: center;
}

.color-picker > * {
  margin-bottom: 1em;
}

.alpha, .hue {
  display: flex;
  justify-content: center;
}

.alpha input, .hue input {
  flex-grow: 1;
  margin-left: 15px;
}

.checkers {
  position: absolute;
  z-index: -1;
  top: 0; bottom: 0; left: 0; right: 0;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='50' height='50'><rect fill='black' x='0' y='0' width='50%' height='50%'/><rect fill='white' x='50%' y='0' width='50%' height='50%'/><rect fill='white' x='0' y='50%' width='50%' height='50%'/><rect fill='black' x='50%' y='50%' width='50%' height='50%'/></svg>");
}

form div {
  display: inline-block;
}

form input {
  margin: 0 5px;
  max-width: 6em;
}

form input[type="number"] {
  max-width: 3em;
}

.icon {
  display: inline-block;
  width: 30px;
  height: 30px;
  line-height: 30px;
  border: 1px solid black;
  border-radius: 100%;
  background-color: white;
}

.icon.alpha {
  background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10'><rect fill='black' x='0' y='0' width='50%' height='50%'/><rect fill='white' x='50%' y='0' width='50%' height='50%'/><rect fill='white' x='0' y='50%' width='50%' height='50%'/><rect fill='black' x='50%' y='50%' width='50%' height='50%'/></svg>");
}

.icon.hue {
  background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
}

React

const COLOR_MODES = {
  hex: {
  	fields: [
    	{ name: 'hex', type: 'text', label: 'Hexadecimal' }
		],
    encode: hex => [hex],
    decode: ([hex]) => hex,
    normalize: normalizeHex,
  },
	rgb: {
  	fields: [
    	{ name: 'r', type: '8bit', label: 'Red' },
      { name: 'g', type: '8bit', label: 'Green' },
      { name: 'b', type: '8bit', label: 'Blue' },
    ],
    encode: hex2rgb,
    decode: rgb2hex,
	},
  hsl: {
  	fields: [
    	{ name: 'h', type: 'polar', label: 'Hue' },
      { name: 's', type: 'percent', label: 'Saturation' },
      { name: 'l', type: 'percent', label: 'Lightness' },
    ],
    encode: hex2hsl,
    decode: hsl2hex,
	},
}

const COLOR_FIELDS = {
	'text': { type: 'text' },
	'8bit': { type: 'number', min: 0, max: 255 },
  'percent': { type: 'number', min: 0, max: 100 },
  'polar': { type: 'number', min: 0, max: 360 },
}

function convert(src, dst, values) {
	if (src === dst) return values
	const srcMode = COLOR_MODES[src]
  const dstMode = COLOR_MODES[dst]
  return dstMode.encode(srcMode.decode(values))
}

class Picker extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
			mode: 'hex',
			values: ['#ffff00'],
      alpha: 100,
    }
  }
  
  setValues = (values) => {
  	this.setState({values})
  }
  
	setAlpha = (alpha) => {
  	this.setState({alpha})
  }
  
	switchMode = () => {
  	const modes = Object.keys(COLOR_MODES)
    const index = modes.findIndex(m => m === this.state.mode)
		const mode = modes[(index + 1) % modes.length]
    const values = convert(
    	this.state.mode,
      mode,
      this.state.values
		)
		this.setState({mode, values})
  }
 
  render() {
    return (
      <div className="color-picker">
        <Color
          mode={this.state.mode}
          values={this.state.values}
          alpha={this.state.alpha} />
        <Alpha
          alpha={this.state.alpha}
          setAlpha={this.setAlpha} />
        <HueSlider
          mode={this.state.mode}
         ...