How can I make a circular input type=color element?

Viewed 754

I simply want to style an <input type="color"> element so that it's a circle without changing its on-click behavior.

My first approach was to just style it with CSS, but this requires the use of ::-webkit-color-swatch/::-moz-color-swatch which are non-standard features and have poor browser support.

const randomColor = () => 
    '#' + ((1 << 24) * Math.random() | 0).toString(16).padStart(6, '0');
const color = document.querySelector('#color');
color.addEventListener('click', event => {
  event.currentTarget.value = randomColor();
  event.currentTarget.style.backgroundColor = event.currentTarget.value;
});
color.addEventListener('input', event => {
  event.currentTarget.style.backgroundColor = event.currentTarget.value;
});
#color {
  border-radius: 25px;
  height: 50px;
  padding: 0;
  width: 50px;
}
#color::-webkit-color-swatch {
  display: none;
}
#color::-moz-color-swatch {
  display: none;
}
<input id="color" type="color">

My second approach was to hide the color selector and trigger its click handler from a div styled to look like a circle instead. The problem with this approach is that the event handler passed to the input element doesn't retain its x and y coordinates, so Chrome shows the color selector in the top left corner of the window rather than under the element.

const randomColor = () => 
    '#' + ((1 << 24) * Math.random() | 0).toString(16).padStart(6, '0');
const div = document.querySelector('#div');
div.addEventListener('click', event => {
  const selector = event.currentTarget.firstElementChild;
  selector.value = randomColor();
  // This doesn't pass the x and y coordinates.
  selector.click();
  // This alternative approach doesn't work either.
  //selector.dispatchEvent(new MouseEvent(event.type, event));
  event.currentTarget.style.backgroundColor = selector.value;
});
div.addEventListener('input', event => {
  event.currentTarget.style.backgroundColor = event.currentTarget.firstElementChild.value;
});
#div {
  border: 1px solid;
  border-radius: 25px;
  height: 50px;
  padding: 0;
  width: 50px;
}
#color {
  display: none;
}
<div id="div">
  <input id="color" type="color">
</div>

Is there a standards-compliant approach to this that doesn't break Chrome's placement of the color selector box?

2 Answers

Instead of hiding the native input using display: none, you can use opacity: 0 and maybe also pointer-events: none, so that the modal opens up in the position you want, in this case 8px below the circle:

const randomColor = () => '#' + ((1 << 24) * Math.random() | 0).toString(16).padStart(6, '0');

const div = document.querySelector('#div');
const input = document.querySelector('#color');

div.addEventListener('click', event => {
  input.value = div.style.backgroundColor = randomColor();
  input.click();
});

input.addEventListener('input', () => {
  div.style.backgroundColor = input.value;
});
#div {
  position: relative;
  border: 1px solid;
  border-radius: 25px;
  height: 50px;
  padding: 0;
  width: 50px;
  overflow: hidden;
}

#color {
  position: absolute;
  top: 0;
  left: 0;
  height: calc(100% + 8px);
  width: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  opacity: 0;
  pointer-events: none;
}
<div id="div">
  <input id="color" type="color">
</div>

Actually, as in this case, the native element is not hidden (it's just transparent), you can get rid of both pointer-events: none and selector.click():

const randomColor = () => '#' + ((1 << 24) * Math.random() | 0).toString(16).padStart(6, '0');

const div = document.querySelector('#div');
const input = document.querySelector('#color');

// This could also be input.addEventListener(...):

div.addEventListener('click', event => {
  input.value = div.style.backgroundColor = randomColor();
});

input.addEventListener('input', () => {
  div.style.backgroundColor = input.value;
});
#div {
  position: relative;
  border: 1px solid;
  border-radius: 25px;
  height: 50px;
  padding: 0;
  width: 50px;
  overflow: hidden;
}

#color {
  position: absolute;
  top: 0;
  left: 0;
  height: calc(100% + 8px);
  width: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  opacity: 0;
}
<div id="div">
  <input id="color" type="color">
</div>

By using <label> for our visible element, we don't need any JavaScript. The native input behavior is more or less preserved. That helps with accessibility too since it points user interaction to the actual input element that assistive technology can recognize.

#color {
  opacity: 0;
  position: absolute;
  pointer-events: none;
  height: 50px;
}

.colorBtn {
  display: inline-block;
  height: 50px;
  width: 50px;
  border-radius: 25px;
  background: turquoise;
  border: 1px solid grey;
}


/* you'll want to add some interaction styles */
.colorBtn:hover, 
#color:focus + .colorBtn {
  border-color: black;
}

#color:active + .colorBtn {
  filter: brightness(90%);
}
<input id="color" type="color" />
<label class="colorBtn" for="color"><label/>

Related