Appearance
ColorPicker Component Code ​
Dependencies
This component requires:
- Vue 3 with Composition API
- Vuetify 3 with VColorPicker component
Full Component Code ​
vue
<template>
<div
class="color-picker-container"
ref="colorPickerContainer"
tabindex="-1"
@blur="handleBlur"
>
<div class="input-wrapper" @click.stop="toggleColorPicker">
<div class="custom-color-input-wrapper">
<slot name="formattedColor">
<div
class="selected-color-preview"
:style="{ backgroundColor: selectedColor || '#FFFFFF' }"
></div>
{{ formattedValue }}
</slot>
</div>
<div
ref="colorPickerMenu"
v-show="isColorPickerOpen"
class="color-picker-wrapper"
:style="dropdownPositioning"
>
<v-color-picker
v-model="selectedColor"
:elevation="elevation"
:hide-inputs="hideInputs"
:hide-canvas="hideCanvas"
:hide-sliders="hideSliders"
:show-swatches="showSwatches"
:swatches-max-height="swatchesMaxHeight"
:bg-color="bgColor"
:width="width"
@update:modelValue="handleColorChange"
@click.stop
></v-color-picker>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, nextTick, toRefs } from "vue";
const props = defineProps({
value: {
type: String,
default: "#FFFFFF"
},
placeholder: {
type: String,
required: false,
default: "Select color",
},
disabled: {
type: Boolean,
required: false,
default: false,
},
hideInputs: {
type: Boolean,
required: false,
default: false,
},
hideCanvas: {
type: Boolean,
required: false,
default: false,
},
hideSliders: {
type: Boolean,
required: false,
default: false,
},
showSwatches: {
type: Boolean,
required: false,
default: true,
},
swatches: {
type: Array,
required: false,
default: () => [],
},
elevation: {
type: Number,
required: false,
default: 1,
},
scale: {
type: Number,
required: false,
default: 1,
},
emitColorOnCloseDropdown: {
type: Boolean,
required: false,
default: false,
},
bgColor: {
type: String,
required: false,
default: 'white',
},
width: {
type: [Number, String],
required: false,
default: 200,
},
swatchesMaxHeight: {
type: [Number, String],
required: false,
default: 100,
},
});
const emit = defineEmits(["onSelectColor"]);
const { value } = toRefs(props);
let isColorPickerOpen = ref(false);
let selectedColor = ref(props.value || null);
let colorPickerMenu = ref(null);
let colorPickerContainer = ref(null);
let dropdownPositioning = ref(null);
let isContainerInverted = ref(false);
const formattedValue = computed(() => {
if (!selectedColor.value) return props.placeholder;
return selectedColor.value;
});
function handleColorChange(color) {
if (!props.emitColorOnCloseDropdown) {
emit("onSelectColor", color);
console.log(color, "Emitted on Color Change");
return;
}
}
function toggleColorPicker() {
if (props.disabled) return;
isColorPickerOpen.value = !isColorPickerOpen.value;
if (isColorPickerOpen.value) {
openDropDownUpwardsOrDownWards();
}
}
function handleBlur(e) {
let { relatedTarget } = e;
if (
colorPickerContainer.value &&
(colorPickerContainer.value.contains(relatedTarget) ||
colorPickerContainer.value.isEqualNode(relatedTarget))
) {
colorPickerContainer.value.focus();
return;
}
if (props.emitColorOnCloseDropdown && selectedColor.value) {
emit("onSelectColor", selectedColor.value);
console.log(selectedColor.value, "Emitted on Close Dropdown");
}
isColorPickerOpen.value = false;
}
function openDropDownUpwardsOrDownWards() {
nextTick(() => {
if (isColorPickerOpen.value && colorPickerMenu.value) {
const dropdownRect = colorPickerMenu.value.getBoundingClientRect();
const colorPickerContainerRect =
colorPickerContainer.value.getBoundingClientRect();
const viewportHeight = window.innerHeight;
const viewportWidth = window.innerWidth;
// Calculate available space below the input
const spaceBelow = viewportHeight - colorPickerContainerRect.bottom;
const spaceAbove = colorPickerContainerRect.top;
// Open downwards if there's enough space, otherwise open upwards
isContainerInverted.value =
spaceBelow < dropdownRect.height && spaceAbove > dropdownRect.height;
// Check if dropdown would overflow right
const rightSpace =
viewportWidth - (colorPickerContainerRect.left + dropdownRect.width);
const leftPosition =
rightSpace < 0
? colorPickerContainerRect.left + rightSpace - 14
: colorPickerContainerRect.left;
dropdownPositioning.value = {
top: isContainerInverted.value
? colorPickerContainerRect.top - dropdownRect.height - 8 + "px"
: colorPickerContainerRect.top +
colorPickerContainerRect.height +
8 +
"px",
left: leftPosition + "px",
};
}
});
}
</script>
<style lang="scss" scoped>
.color-picker-container {
position: relative;
}
.color-picker-wrapper {
position: fixed;
z-index: 1000;
background: white;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transform-origin: top left;
}
.custom-color-input-wrapper {
cursor: pointer;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
min-height: 40px;
display: flex;
align-items: center;
outline: none;
&:focus {
border-color: #007bff;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
}
.selected-color-preview {
width: 24px;
height: 24px;
border-radius: 4px;
margin-right: 8px;
border: 1px solid #ddd;
}
</style>