Skip to content

Proactive Toggle toggle

When it’s disabled, the toggle will stubbornly flip itself back just to mess with you. ( ´థ౪థ)

Inspired by the Useless machine — this little piece of trash is peak maker romance. (´,,•ω•,,)

Why a cat paw? Because a cat paw is the cheekiest, most smackable little hand I could think of. ヾ(◍'౪`◍)ノ゙

Usage Examples

Basic Usage

When the toggle is disabled and you try to flip it, the cat paw pops out and switches it right back. (◜௰◝)y

View example source code
vue
<template>
  <div class="w-full flex flex-col gap-4 border border-gray-200 rounded-xl p-6">
    <base-checkbox
      v-model="disabled"
      :label="t('disableToggle')"
      class="border rounded p-4"
    />

    <div class="flex flex-1 items-center justify-center">
      <toggle-proactive
        v-model="value"
        :disabled="disabled"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import BaseCheckbox from '../../base-checkbox.vue'
import ToggleProactive from '../toggle-proactive.vue'

const { t } = useI18n()
const disabled = ref(false)
const value = ref(false)
</script>

Component Props

Style it however you like — the appearance is fully customizable.

View example source code
vue
<template>
  <div class="w-full flex flex-col items-center gap-10 border border-gray-200 rounded-xl p-8">
    <toggle-proactive
      :model-value="false"
      disabled
      size="3rem"
      fur-color="#DFC57B"
      pad-color="#FFF"
    />

    <toggle-proactive
      :model-value="true"
      disabled
      size="6rem"
      track-inactive-class="bg-red-400"
      track-active-class="bg-[#DFDFDF]"
      fur-color="#8D6F64"
      pad-color="#000"
    />

    <toggle-proactive
      :model-value="false"
      disabled
      size="4rem"
      track-active-class="bg-[#7DDAEA]"
      fur-color="#F3F2F2"
    />
  </div>
</template>

<script setup lang="ts">
import ToggleProactive from '../toggle-proactive.vue'
</script>

Impossible Requests

A cute but polite way to say “no” to impossible client requests. ヾ(◍'౪`◍)ノ゙

View example source code
vue
<template>
  <div class="w-full flex-center border border-gray-200 rounded-xl p-10">
    <div class="flex flex-col gap-4">
      <label
        v-for="state in stateList"
        :key="state.label"
        class="flex cursor-pointer items-center justify-end gap-5"
      >
        <div class="text-2xl">
          {{ state.label }}
        </div>

        <toggle-proactive
          ref="toggleRefList"
          v-model="state.value"
          v-bind="colorData"
          size="3.5rem"
        />
      </label>
    </div>
  </div>
</template>

<script setup lang="ts">
import { useCycleList } from '@vueuse/core'
import { pipe, reduce, sample } from 'remeda'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import ToggleProactive from '../toggle-proactive.vue'

interface State {
  value: boolean;
  label: string;
}

type Toggle = InstanceType<typeof ToggleProactive>

const { t } = useI18n()
const toggleRefList = ref<Toggle[]>()

const {
  state: colorData,
  next: nextColor,
} = useCycleList([
  {
    furColor: '#7DDAEA',
    padColor: '#000',
  },
  {
    furColor: '#FAFAFA',
    padColor: '#FFA5A5',
  },
  {
    furColor: '#DFC57B',
    padColor: '#000',
  },
  {
    furColor: '#8D6F64',
    padColor: '#FFA5A5',
  },
  {
    furColor: '#444',
    padColor: '#FFA5A5',
  },
  {
    furColor: '#F3F2F2',
    padColor: '#000',
  },
])

const stateList = ref<State[]>([
  {
    label: t('wantFast'),
    value: false,
  },
  {
    label: t('wantGood'),
    value: false,
  },
  {
    label: t('wantCheap'),
    value: false,
  },
])
const booleanList = computed(
  () => stateList.value.map((state) => state.value),
)

watch(booleanList, (value, oldValue) => {
  const allTrue = value.every((v) => v)
  if (!allTrue) {
    return
  }

  const targetIndex = pipe(
    oldValue,
    /** 排除最後一個切換的開關 */
    reduce(
      (acc: number[], boolValue, i) => boolValue ? [...acc, i] : acc,
      [],
    ),
    sample(1),
    ([i]) => i ?? 0,
  )

  nextColor()

  toggleRefList.value?.[targetIndex]?.toggle()
})
</script>

<i18n lang="json">
{
  "zh-hant": {
    "wantFast": "要快",
    "wantGood": "要好",
    "wantCheap": "要便宜"
  },
  "en": {
    "wantFast": "Fast",
    "wantGood": "Good",
    "wantCheap": "Cheap"
  }
}
</i18n>

How It Works

Heads up! Σ(ˊДˋ;)

Please don’t set overflow to hidden, or the poor cat paw will get brutally chopped off.

The animation is done with anime.js and an SVG.

There’s a fun little detail: at first, the cat paw is hiding behind the toggle (both the arm and elbow are behind it).
When the toggle animation plays, the arm stays behind the toggle, but the elbow sneaks out in front of it.

How can objects inside an SVG suddenly change their stacking order like that?
Take a guess at how this effect is achieved. (´,,•ω•,,)

I’ll leave it as a tiny mystery for now — if you’re curious, check out the source code or leave a comment with your guess. ( ´ ▽ ` )ノ

Source Code

API

Props

interface Props {
  modelValue: boolean;
  disabled?: boolean;
  /** @default '4rem' */
  size?: string;

  /** @default 'rounded-full' */
  trackClass?: string;
  /** @default 'bg-[#DFDFDF]' */
  trackInactiveClass?: string;
  /** @default 'bg-green-500' */
  trackActiveClass?: string;
  /** @default 'bg-white' */
  thumbClass?: string;
  /** @default '' */
  thumbInactiveClass?: string;
  /** @default '' */
  thumbActiveClass?: string;

  /** @default '#444' */
  furColor?: string;
  /** @default '#FFA5A5' */
  padColor?: string;
}

Emits

interface Emits {
  'update:modelValue': [value: boolean];
}

Methods

interface Expose {
  /** 觸發切換動畫 */
  toggle: () => Promise<void>;
}

v0.54.2