Skip to content

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>