Skip to content

Switch Component Code ​

Dependencies

This component requires:

  • Vue 3 with Composition API

Full Component Code ​

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

const emit = defineEmits(["update:modelValue", "change"]);
const props = defineProps({
  modelValue: {
    type: Boolean,
  },
  value: {
    type: Boolean,
  },
  disabled: {
    type: Boolean,
    default: false,
  },
  readonly: {
    type: Boolean,
    default: false,
  },
  variant: {
    type: String,
    default: "default", // default, success
  },
  size: {
    type: String,
    default: "xl", //xs, sm, md, lg, xl
  },
  label: {
    type: String,
    default: "",
  },
  labelPosition: {
    type: String,
    default: "right", //left, right
  },
  inset: {
    type: Boolean,
    default: false,
  },
  dotLabels: {
    type: Object,
    default: () => null,
    // {
    //   true: 'On',
    //   false: 'Off',
    // }
  },
});

const internalValue = ref(props.modelValue || props.value || false);

watch(
  [() => props.modelValue, () => props.value],
  ([newModelValue, newValue]) => {
    internalValue.value = newModelValue ?? newValue;
  }
);

const handleSwitchChange = () => {
  if (props.readonly || props.disabled) return;
  const newValue = !internalValue.value;

  internalValue.value = newValue;
  if (props.modelValue !== undefined) {
    emit("update:modelValue", newValue);
  }
  emit("change", newValue);
};
</script>

<template>
  <label
    class="switch-container"
    :class="{
      disabled: disabled,
      readonly: readonly,
      checked: internalValue,
      [size]: true,
      inset: inset,
      [variant]: true,
    }"
    @click.prevent="handleSwitchChange"
  >
    <slot name="left-label" v-if="label && labelPosition === 'left'">
      {{ label }}
    </slot>
    <input
      type="checkbox"
      :checked="internalValue"
      hidden
      :readonly="readonly"
      :disabled="disabled"
    />
    <span class="switch-slider">
      {{ dotLabels?.[internalValue] }}
      <span class="switch-slider-dot"> </span>
    </span>
    <slot name="right-label" v-if="label && labelPosition === 'right'">
      {{ label }}
    </slot>
  </label>
</template>

<style lang="scss" scoped>
.switch-container {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  cursor: pointer;

  *,
  ::before *,
  ::after * {
    box-sizing: border-box;
  }

  input {
    display: none;

    &:checked + .switch-slider {
      justify-content: flex-start;

      .switch-slider-dot {
        transform: translateX(-100%);
        margin: 0;
      }
    }
  }

  .switch-slider {
    position: relative;
    min-width: 4rem;
    height: 2rem;
    border-radius: 1rem;
    transition: all 0.3s ease;
    display: flex;
    align-items: center;
    cursor: pointer;
    justify-content: flex-end;

    .switch-slider-dot {
      position: absolute;
      left: 0;
      border-radius: 50%;
      transition: all 0.3s ease;
      transform: translateX(0);
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 0.5rem;
      overflow: hidden;
    }
  }

  // Variant styles
  &.default {
    .switch-slider {
      background-color: grey;
      color: #000;
    }

    input:checked + .switch-slider {
      background-color: grey;
    }

    &:not(.inset) {
      .switch-slider-dot {
        background-color: white;
      }

      input:checked + .switch-slider .switch-slider-dot {
        background-color: white;
      }
    }

    &.inset {
      .switch-slider-dot {
        background-color: white;
      }

      input:checked + .switch-slider .switch-slider-dot {
        background-color: white;
      }
    }
  }

  &.success {
    .switch-slider {
      background-color: #ccffcc;
      color: #000;
    }

    input:checked + .switch-slider {
      background-color: #44ff44;
    }

    &:not(.inset) {
      .switch-slider-dot {
        background-color: white;
      }

      input:checked + .switch-slider .switch-slider-dot {
        background-color: white;
      }
    }

    &.inset {
      .switch-slider-dot {
        background-color: #ccffcc;
      }

      input:checked + .switch-slider .switch-slider-dot {
        background-color: #44ff44;
      }
    }
  }

  &.inset {
    input + .switch-slider .switch-slider-dot {
      transform: translateX(-50%);
      left: 0;
      margin: 0;
    }

    input:checked + .switch-slider .switch-slider-dot {
      left: calc(100%);
      margin: 0;
    }
  }

  // Size variations
  &.xs {
    font-size: 0.75rem;
    .switch-slider {
      min-width: 2.5rem;
      height: 1.25rem;
      font-size: 0.375rem;
      padding-inline: 0.25rem;

      .switch-slider-dot {
        width: 1rem;
        height: 1rem;
        margin: 0 0.1875rem;
      }
    }

    input:checked + .switch-slider .switch-slider-dot {
      left: calc(100% - 0.1875rem);
    }

    &.inset {
      .switch-slider {
        min-width: 1.75rem;
        height: 0.75rem;
        margin-right: 0.5rem;
        font-size: 0.375rem;

        .switch-slider-dot {
          width: 1rem;
          height: 1rem;
          margin: 0 0.1875rem;
          box-shadow: 0px 2px 1px -1px rgba(0, 0, 0, 0.2),
            0px 1px 1px 0px rgba(0, 0, 0, 0.14),
            0px 1px 3px 0px rgba(0, 0, 0, 0.12);
        }
      }
    }
  }

  &.sm {
    font-size: 0.875rem;
    .switch-slider {
      min-width: 3rem;
      height: 1.5rem;
      font-size: 0.4375rem;
      padding-inline: 0.3125rem;

      .switch-slider-dot {
        width: 1.25rem;
        height: 1.25rem;
        margin: 0 0.1875rem;
      }
    }

    input:checked + .switch-slider .switch-slider-dot {
      left: calc(100% - 0.1875rem);
    }

    &.inset {
      .switch-slider {
        min-width: 2.125rem;
        height: 0.875rem;
        margin-right: 0.5rem;
        font-size: 0.4375rem;

        .switch-slider-dot {
          width: 1.125rem;
          height: 1.125rem;
          margin: 0 0.1875rem;
          box-shadow: 0px 2px 1px -1px rgba(0, 0, 0, 0.2),
            0px 1px 1px 0px rgba(0, 0, 0, 0.14),
            0px 1px 3px 0px rgba(0, 0, 0, 0.12);
        }
      }
    }
  }

  &.md {
    font-size: 1rem;
    .switch-slider {
      min-width: 4rem;
      height: 2rem;
      font-size: 0.5625rem;
      padding-inline: 0.375rem;

      .switch-slider-dot {
        width: 1.5rem;
        height: 1.5rem;
        margin: 0 0.25rem;
        font-size: 0.5625rem;
      }
    }

    input:checked + .switch-slider .switch-slider-dot {
      left: calc(100% - 0.25rem);
    }

    &.inset {
      .switch-slider {
        min-width: 2.5rem;
        height: 1rem;
        margin-right: 0.625rem;
        font-size: 0.5625rem;
        padding-inline: 0.25rem;

        .switch-slider-dot {
          width: 1.25rem;
          height: 1.25rem;
          margin: 0 0.25rem;
          box-shadow: 0px 2px 1px -1px rgba(0, 0, 0, 0.2),
            0px 1px 1px 0px rgba(0, 0, 0, 0.14),
            0px 1px 3px 0px rgba(0, 0, 0, 0.12);
        }
      }
    }
  }

  &.lg {
    font-size: 1.125rem;
    .switch-slider {
      min-width: 5rem;
      height: 2.5rem;
      border-radius: 1.25rem;
      font-size: 0.6875rem;
      padding-inline: 0.5rem;

      .switch-slider-dot {
        width: 2rem;
        height: 2rem;
        margin: 0 0.375rem;
      }
    }

    input:checked + .switch-slider .switch-slider-dot {
      left: calc(100% - 0.375rem);
    }

    &.inset {
      .switch-slider {
        min-width: 3.125rem;
        height: 1rem;
        margin-right: 0.625rem;
        padding-inline: 0.375rem;
        font-size: 0.6875rem;

        .switch-slider-dot {
          width: 1.5rem;
          height: 1.5rem;
          margin: 0 0.375rem;

          box-shadow: 0px 2px 1px -1px rgba(0, 0, 0, 0.2),
            0px 1px 1px 0px rgba(0, 0, 0, 0.14),
            0px 1px 3px 0px rgba(0, 0, 0, 0.12);
        }
      }
    }
  }

  &.xl {
    font-size: 1.25rem;
    .switch-slider {
      min-width: 6rem;
      height: 3rem;
      border-radius: 1.5rem;
      font-size: 0.875rem;
      padding-inline: 0.5rem;
      .switch-slider-dot {
        width: 2.5rem;
        height: 2.5rem;
        margin: 0 0.375rem;
      }
    }

    input:checked + .switch-slider .switch-slider-dot {
      left: calc(100% - 0.375rem);
    }

    &.inset {
      .switch-slider {
        min-width: 3.5rem;
        height: 1.25rem;
        margin-right: 0.625rem;
        font-size: 0.75rem;

        .switch-slider-dot {
          width: 1.75rem;
          height: 1.75rem;
          margin: 0 0.375rem;

          box-shadow: 0px 2px 1px -1px rgba(0, 0, 0, 0.2),
            0px 1px 1px 0px rgba(0, 0, 0, 0.14),
            0px 1px 3px 0px rgba(0, 0, 0, 0.12);
        }
      }
    }
  }

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

  &.readonly {
    pointer-events: none;
  }
}
</style>