iOS-like animated toggle with TailwindCSS
As always, I really like the content that Sam Selikoff is creating. This time he dropped a video where he showed how to create a toggle that feels and behaves like the native iOS toggle.
Demo
Code
Sam is using the React Aria Components package to make use of all the different states of the toggle to easily apply some styles that are depending on these. Unfortunately, there's no Vue equivalent out there, so I tried to look at the source code and implement a Vue Aria toggle component that sets all the states.
Then, to make it more reusable, I created the <CustomToggle />
component that is using the <AriaToggle />
. To detect interactions and state, I use composables provided by the great VueUse library.
iOS Toggle
<template>
<label
ref="label"
:data-selected="isSelected || undefined"
:data-hovered="isHovered || undefined"
:data-pressed="pressed || undefined"
:data-disabled="isDisabled || undefined"
:data-focused="focused || undefined"
:data-focus-visible="isFocusVisible || undefined"
>
<input
ref="input"
v-model="isSelected"
type="checkbox"
class="ally-checkbox focus-visible:visible"
@focus="onFocus"
@blur="isFocusVisible = false"
>
<slot />
</label>
</template>
<script setup>
import { useElementHover, useFocus, onKeyDown, onKeyUp, useMousePressed, whenever } from '@vueuse/core'
const props = defineProps({
modelValue: {
type: Boolean,
required: true,
},
})
const emit = defineEmits(['update:modelValue'])
const label = ref(null)
const input = ref(null)
const isHovered = useElementHover(label)
const isFocusVisible = ref(false)
const isDisabled = computed(() => input?.value?.disabled)
const { pressed } = useMousePressed({ target: label })
const { focused } = useFocus(input)
const isSelected = computed({
get() {
return props.modelValue
},
set(value) {
emit('update:modelValue', value)
},
})
function onFocus({ relatedTarget }) {
if (relatedTarget) {
isFocusVisible.value = true
}
}
whenever(focused, () => {
onKeyUp(' ', (e) => {
if (focused.value) {
pressed.value = false
}
})
onKeyDown(' ', (e) => {
if (focused.value) {
pressed.value = true
}
}, { dedupe: true })
})
</script>
<style>
.ally-checkbox {
border: 0;
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
white-space: nowrap;
}
</style>
Let me know what you think about it!