Skip to content

Checkbox Component Code ​

Dependencies

This component requires:

  • Vue 3 with Composition API

Full Component Code ​

vue
<script setup>
import { computed, ref, toRefs } from "vue";

const emit = defineEmits(["update:modelValue", "change"]);

const props = defineProps({
  size: {
    type: String,
    default: "md",
    validator: (value) => ["xs", "sm", "md", "lg", "xl"].includes(value),
  },
  disabled: {
    type: Boolean,
    default: false,
  },
  readonly: {
    type: Boolean,
    default: false,
  },
  label: {
    type: String,
    default: "",
  },
  value: {
    type: [String, Number, Boolean],
    required: true,
    default: "",
  },
  modelValue: {
    type: Array,
    required: false,
    default: () => [],
  },
  selected: {
    type: Array,
    required: false,
    default: () => [],
  },
  valueComparator: {
    type: Function,
    default: (a, b) => Array.isArray(a) && a.includes(b),
  },
  variant: {
    type: String,
    default: "default",
  },
  allItems: {
    type: Array,
    default: () => [],
  },
  valueKey: {
    type: String,
    default: "",
  },
});

const {
  disabled,
  readonly,
  modelValue,
  selected,
  valueComparator,
  value: inputValue,
} = toRefs(props);

const internalValue = computed(() => {
  let val = modelValue.value?.length ? modelValue.value : selected.value;
  return Array.isArray(val) ? val : [];
});

// Determine if this radio should be selected
const isChecked = computed(() => {
  if (!internalValue.value?.length) return false;
  if (inputValue.value === "selectAll") {
    // Check if all items from allItems are present in internalValue
    if (internalValue.value?.length !== props.allItems?.length) return false;
    return props.allItems.every((item) => {
      const itemValue = props.valueKey ? item[props.valueKey] : item;
      return internalValue.value.includes(itemValue);
    });
  }
  return valueComparator.value(internalValue.value, inputValue.value);
});

// Handle click
function handleClick(event) {
  // Skip if disabled or readonly
  if (disabled.value || readonly.value) return;

  const isSelected = isChecked.value;
  const currentValues = Array.isArray(internalValue.value)
    ? internalValue.value
    : [];

  let newValue;
  if (inputValue.value === "selectAll") {
    // If selectAll is clicked and not all items are selected, select all
    // Otherwise, empty the array
    if (currentValues.length < props.allItems.length) {
      if (props.valueKey) {
        newValue = props.allItems.map((item) => item[props.valueKey]);
      } else {
        newValue = [...props.allItems];
      }
    } else {
      newValue = [];
    }
  } else {
    // Normal checkbox behavior
    if (isSelected) {
      newValue = currentValues.filter((v) => v !== inputValue.value);
    } else {
      newValue = [...currentValues, inputValue.value];
    }
  }
  console.log(newValue, inputValue.value);
  emit("update:modelValue", newValue);
  emit("change", newValue, inputValue.value, event);
}
</script>

<template>
  <label
    class="checkbox-container"
    :class="[size, variant, { disabled, readonly }]"
  >
    <input
      hidden
      type="checkbox"
      :value="inputValue"
      :checked="isChecked"
      :disabled="disabled"
      @click="handleClick"
    />
    <slot name="icon" :props="{ isChecked }">
      <div class="checkbox">
        <div class="inner-tick"></div>
      </div>
    </slot>
    <slot name="label" :props="{ isChecked }">{{ label }}</slot>
  </label>
</template>

<style lang="scss" scoped>
.checkbox-container {
  *,
  *::before,
  *::after {
    box-sizing: border-box;
  }

  display: flex;
  align-items: center;
  gap: 0.5rem;
  cursor: pointer;
  user-select: none;
  color: #000;
  position: relative;

  input {
    display: none;

    &:checked + .checkbox {
      .inner-tick {
        visibility: visible;
        opacity: 1;
      }
    }
  }

  &.disabled {
    pointer-events: none;
    opacity: 0.5;
  }

  &.readonly {
    pointer-events: none;
  }

  .checkbox {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 0.0625rem solid;
    border-radius: 0.125rem;
    background-color: transparent;
    .inner-tick {
      border-bottom-right-radius: 0.0625rem;
      width: 33%;
      height: 80%;
      border-bottom: 0.125rem solid;
      border-right: 0.125rem solid;
      transform: rotate(45deg);
      visibility: hidden;
      opacity: 0;
    }

    &:active {
      opacity: 0.7;
    }
  }
  // Sizes
  &.xs {
    font-size: 0.625rem;
    .checkbox {
      width: 0.75rem;
      height: 0.75rem;
    }
  }
  &.sm {
    font-size: 0.75rem;
    .checkbox {
      width: 0.875rem;
      height: 0.875rem;
    }
  }
  &.md {
    font-size: 0.875rem;
    .checkbox {
      width: 1rem;
      height: 1rem;
    }
  }
  &.lg {
    font-size: 1rem;
    .checkbox {
      width: 1.125rem;
      height: 1.125rem;
    }
  }
  &.xl {
    font-size: 1.125rem;
    .checkbox {
      width: 1.25rem;
      height: 1.25rem;
    }
  }

  // Variants
  &.default {
    color: #000;
    .checkbox {
      border-color: #000;
      &:hover {
        background-color: #000 !important;
      }
    }
    input:checked + .checkbox {
      background-color: #000 !important;
    }
    .inner-tick {
      border-color: #fff;
    }
  }

  &.success {
    color: #22c55e;
    .checkbox {
      border-color: #22c55e;
      &:hover {
        background-color: #22c55e !important;
      }
    }
    input:checked + .checkbox {
      background-color: #22c55e !important;
    }
    .inner-tick {
      border-color: #fff;
    }
  }
}
</style>