Slick and perfomant Vue 3 color picker component whose goal is to replace <input type="color">
🚀  Live demo  🚀
Forget about color conversions: vue-color-input does it for you. Unlike <input type="color">
(which only understands hex) vue-color-input supports all commonly used color models, and by default will output color in the same format that was passed as input. It also has support for alpha channel, unless you specifically disable it.
HTML's native color input is annoying to style. Most likely you'll have to get tricky hiding the original input & binding click event to a presentable-looking div. But it only gets you halfway there cause the color picker popup window is still out of reach and it might look way different in different browsers.
With vue-color-input this poblem is solved. It looks pretty out of the box with the default styles, but it's also intuitive & straight-forward to customize from css.
Not only that native color input looks different in different browsers, it also operates differently, and in some cases it's just not what you expect it to be. Yes, I'm looking at you, Safari. vue-color-input delivers a color picker that looks and performs the same regardless of browser.
vue-color-input combines minimalist approach with comprehensive functionality. You can customize and extend it to your liking, set it up with additional properties, but you don't have to. It works as expected out of the box just as well, with only v-model
provided.
npm i vue-color-input
import ColorInput from 'vue-color-input'
<script src="https://unpkg.com/vue-color-input@latest"></script>
// install it with use()
app.use(ColorInput)
// OR register component globally
app.component('ColorInput', ColorInput)
// OR locally
export.default {
components: { ColorInput }
}
<color-input v-model="color" />
This is where you supply the variable holding the color to be adjusted by end user.
Model value is allowed to be changed externally too, vue-color-input will adjust accordingly.
When first initialized and every time v-model value updates from outside the component, incoming color format is stored to be matched by output.
Under the hood vue-color-input uses tinycolor2
for color conversion. So everything tinycolor accepts as input, is valid here as well (both string and object).
By default output will be a string or an object in the same color model as the initial value.
For example:
// in parent component
export.default {
data() {
color: "rgb(50, 150, 150)"
}
}
<!-- in template -->
<color-input v-model="color" />
User adjusts hue to 0
, now color
becomes
"rgb(150, 50, 50)"
Then user adjusts alpha to 0.5
, color
becomes
"rgba(150, 50, 50, 0.5)"
Let's say color
property was initialy set to be an object:
// in parent component
export.default {
data() {
color: { "h": 350, "s": 1, "l": 0.8 }
}
}
In the same scenario the resulting output would be
{ "h": 0, "s": 1, "l": 0.8, "a": 0.5 }
vue-color-input will always try to output color in the same color model as the initial value (unless target format is specified explicitly by format
property.
However in some cases that would not be possible. For those colors it will fall back to different formats.
If initial color format was name
(e.g. "purple"
) or hex
(e.g. "#800080"
), and then alpha is changed to be less than 1
, output will be formatted as rgba
:
"#cd5c5c" // hex input
/* user changes alpha to 0.9 */
"rgba(205, 92, 92, 0.9)" // rgba output
Note: this behavior does not apply if format
property is explicitly set to be hex
or name
.
Note 2: if initial color format is hex8
(e.g. #800080ff
), output will be hex8
also, unless specified differently by format
property.
If initial color format was name
, but the resulting output color does not have a name equivalent, hex
value will be output instead:
"indianred" // name input
/* user changes hue to 180 */
"#5ccdcc" // hex output
Invalid color initialy diasplays as black. Default output format will be set to rgb
:
"ironmanred" // invalid string input
/* user changes alpha to 0.1 */
"rgba(0, 0, 0, 0.1)" // rgb(a) output
Here you can supply the color format you want the output to be in.
The value consists of two arguments: format & type. The order of two is inconsequential, e.g. both "hsl object"
& "object hsl"
are valid values.
Format is the target color model that the return value is converted to. [ "rgb", "hsv", "hsl", "hex", "hex8", "name" ]
Type is data type of the return value. [ "string", "object" ]
If you want to use v-model value for styling, "string"
type should do the job. On the other hand, if you want to continue processing the data, "object"
is probably more useful.
Hsv & hsl color component values are presented differently in different output types:
"hsl(0, 53%, 58%)" // "hsl string"
{ "h": 0, "s": 0.531, "l": 0.582, "a": 1 } // "hsl object"
Notice how strings contain percent-based values, and object 0-1 floats.
Note that name & hex formats don't support alpha channel. Specifying either of them as target format will prevent vue-color-input from falling back to rgba. Instead, it will disable alpha slider and always return full opacity color.
If this is not the behavior that you want, and you'd rather it fall back to rgba to support alpha, you should not specify the format.
String
[ "rgb", "rgb object", "rgb string",
"hsv", "hsv object", "hsv string",
"hsl", "hsl object", "hsl string",
"name", "name string",
"hex", "hex string",
"hex8", "hex8 string" ]
Note: "name object"
, "hex object"
& "hex8 object"
, make no sense and therefore are illegal.
Note 2: format without type is allowed, type without format is not.
Calculated to match the input.
<color-input v-model="color" format="rgb object" />
This is where you specify the position of the popup color picker window relative to the clickable box.
String
[ "top", "top right", "top left", "top center",
"right top", "right", "right bottom", "right center",
"bottom right", "bottom", "bottom left", "bottom center",
"left top", "left bottom", "left", "left center" ]
Pretty intuitive: the first value is the direction from the box in which the popup will appear, the second is how it will align.
Note: Omitting the second parameter results in center alignment, making "top"
a shortcut for "top center"
"bottom"
<color-input v-model="color" position="right top" />
Setting this to true
will make the initial box nonresponsive to user clicks. The popup will not appear.
However the box will still react to v-model changes, should they come from elsewhere.
Boolean
[ true, false ]
false
<color-input v-model="color" :disabled="!allowColorAdjustment" />
If you set this to true
, alpha slider will be removed from the color picker, and the returned color will always have full opacity.
Specifying name or hex as the target
format
will make this property default totrue
and ignore any passed value.
Boolean
[ true, false ]
false
,
true
if target format is hex or name
<color-input v-model="color" disable-alpha />
With this property you can hide the section of the color picker containing the text inputs.
Boolean
[ true, false ]
false
<color-input v-model="color" disable-text-inputs />
Set this to a custom transition name to override factory enter and leave-to transitions of the popup.
This is not the only way to customize color picker transition.
You can also override default transition classes from css. More details below.
More information about Vue enter/leave transitions here.
String
"picker"
<color-input v-model="color" transition="my-cool-transition" />
.my-cool-transition-enter-from,
.my-cool-transition-leave-to {
transform: rotate(240) scale(.5);
opacity: 0;
}
.my-cool-transition-enter-active,
.my-cool-transition-leave-active {
transition: transform .3s, opacity .3s;
}
As previously mentioned, applying styles to vue-color-input is a breeze.
Default CSS is written with custumizability in mind, so anything you want to style will likely work as expected, and the whole component's layout will not get screwed up by that.
To override factory styles, you should address elemets through .color-input.user
parent selector, e.g:
.color-input.user .box { }
class | description |
---|---|
.color-input | Root element |
.box | Initial clickable box |
.picker-popup | Popup color picker window |
.saturation-area | Picking area where you select saturation and brightness |
.slider | Hue and opacity sliders (track) |
.saturation-pointer | Pointer in the saturation-brightness area |
.slider-pointer | Pointer on a slider |
.text-input | Text inputs of the color picker |
Feel free to scout the HTML for more class names.
Instead of using transition
property with a custom transition name, you can simply override default transition styles.
This can be done in the same manner as with the other classes, e.g:
.color-input.user .picker-popup-enter-from {
transform: translateY(-100%) scale(.1);
}
.color-input.user .picker-popup-leave-to {
transform: scale(3);
}
/* and if you want to change the durations as well */
.color-input.user .picker-popup-enter-active,
.color-input.user .picker-popup-leave-active {
transition: all .5s;
}
More information about Vue enter/leave transitions here.
When clicked on, the box gets what looks like an outline, but in reality its content is scaled down and background is revealed.
Here's what the box element html looks like:
<div class="box [active] [disabled]"> <!-- This has a background -->
<div class="inner transparent"> <!-- This scales down to reveal it -->
<div class="color"></div>
</div>
</div>
To customize this transition, you can use .box.active
in combination with .box.active .inner
.
For example:
.color-input.user .box.active {
/* "outline" color */
background: #0f0f0f;
}
.color-input.user .box.active .inner {
/* different transition effect */
transform: scale(.9) rotate(90deg);
}
.color-input.user .box {
/* make clickable box a 100x100 circle */
width: 100px;
height: 100px;
border-radius: 50px;
}
.color-input.user .picker-popup {
/* dark mode for popup window */
background: #000;
color: #fbfbfb;
/* and make it wide */
width: 400px;
}
.color-input.user .slider {
/* thin out the sliders and make them wider */
height: 2px;
width: 92%;
}
.color-input.user .saturation-area {
/* bigger picking area */
height: 150px;
}
.color-input.user .slider-pointer {
/* make slider pointers square-ish and 10x10 */
border-radius: 4px;
width: 10px;
height: 10px;
}
.color-input.user .saturation-pointer {
/* increase saturation pointer size */
width: 40px;
height: 40px;
}
Here's the base structure of the component:
<div class="color-input">
<div class="box [active] [disabled]"></div>
<div class="picker-popup"></div> <!-- position: absolute -->
</div>
Root element wraps arond the clickable box, but if you want to change box styles, you should select it like this: .color-input.user .box
.
Generally, you should attempt to style the root element only if you want to customize the flow: properties like margin
, position
, display
.
Changing size of the root element independently from the box will mess with how the popup is positioned.
Inline styles will only let you style the root element, which is typically not what you want to style very often.
There is no need to use !important
. Default styles are easily overridable by adding specificity to the selectors with .color-input.user .<classname>
.
And if you use scss that's even more natural with nesting:
.color-input.user {
.box {}
.picker-popup {}
// etc
}
margin
is one of the few properties that should belong to the .color-input
itself.
Setting margin on the .box
instead will increase the space around it inside the root element, and that will mess with how the popup is positioned.
The instance provides hooks for custom event handling.
Most events carry payload with current state of the corresponding color component.
Notice that event data is always passed in hsv format.
event | description | payload |
---|---|---|
pickEnd | color picking process is finished, popup will close now | |
mounted | lifecycle hook, emitted from root component's mounted() | |
beforeUnmount | lifecycle hook, emitted from root component's beforeUnmount() | |
pickStart | color picking process is initiated, popup is opening | |
saturationInputStart | saturation-brightness adjustment has begun. This is only emitted when pointerdown inside saturation-brightness area is registered. This will not emit when text inputs are edited |
current state of saturation & value (hsv){ s: 0.5, v: 0.5 } |
saturationInputEnd | saturation-brightness adjustment has ended. This is only emitted when pointerup of the saturation-brightness area is registered. This will not emit when text inputs are edited |
current state of saturation & value (hsv){ s: 0.5, v: 0.5 } |
saturationInput | saturation-brightness is being adjusted. This will emit every time saturation-brightness is changed, including text inputs |
current state of saturation & value (hsv){ s: 0.5, v: 0.5 } |
hueInputStart | hue adjustment has begun. This is only emitted when pointerdown over the hue slider is registered. This will not emit when hue is changed from text inputs |
current state of hue{ h: 180 } |
hueInputEnd | hue adjustment has ended. This is only emitted when pointerup of the hue slider is registered. This will not emit when hue is changed from text inputs |
current state of hue{ h: 180 } |
hueInput | hue is being adjusted. This will emit every time hue is changed, including text inputs |
current state of hue{ h: 180 } |
alphaInputStart | alpha adjustment has begun. This is only emitted when pointerdown over the alpha slider is registered. This will not emit when alpha is changed from text inputs |
current state of alpha{ a: 0.5 } |
alphaInputEnd | alpha adjustment has ended. This is only emitted when pointerup of the alpha slider is registered. This will not emit when alpha is changed from text inputs |
current state of alpha{ a: 0.5 } |
alphaInput | alpha is being adjusted. This will emit every time alpha is changed, including text inputs |
current state of alpha{ a: 0.5 } |
change | the color has changed by user interaction. This will emit every time any parameter is changed. This will emit when color is changed from text inputs as well, on blur |
current state of all color components{ h: 180, s: 0.5, v: 0.5, a: 0.5 } |
<color-input v-model="color" @mounted="colorInputMountedHandler" @pickStart="colorPickerShowHandler" />
You shouldn't need to manually access instance elements or methods, but if you feel like it, you can.
This can be done by specifying a ref
property on the instance.
The following section implies you have a vue-color-input instance with a ref
property set to "colorInput"
:
<color-input v-model="color" ref="colorInput" />
const colorInput = this.$refs.colorInput // root instance
const picker = colorInput.$refs.picker // popup color picker instance
colorInput.$refs.root // root element
colorInput.$refs.box // box root element
picker.$refs.rootPicker // color picker root element
colorInput.pickStart() // begin color selection (show popup)
colorInput.pickEnd() // end color selection (hide popup)
colorInput.color // tinycolor instance
MIT