doxa-maps-cdn/app/components/public/ToggleSwitch.vue

72 lines
2 KiB
Vue

<script setup lang="ts">
// Accessible on/off switch styled to match the campaigns-server feedback form.
// Used instead of a checkbox for opt-ins where the control should read as a
// preference rather than a form field. The label is rendered by the caller and
// wired up through `label-id` so it stays part of the surrounding copy flow.
const model = defineModel<boolean>({ default: false })
defineProps<{
/** id of the element labelling this switch, for `aria-labelledby`. */
labelId?: string
}>()
</script>
<template>
<button
type="button"
role="switch"
class="toggle-switch"
:class="{ 'is-on': model }"
:aria-checked="model"
:aria-labelledby="labelId"
@click="model = !model"
>
<span class="toggle-switch__thumb" />
</button>
</template>
<style scoped>
.toggle-switch {
--toggle-width: 2.75rem;
--toggle-height: 1.5rem;
--toggle-padding: 0.2rem;
flex: none;
display: inline-flex;
align-items: center;
width: var(--toggle-width);
height: var(--toggle-height);
padding: var(--toggle-padding);
border: none;
border-radius: 100px;
background-color: var(--color-brand-secondary);
cursor: pointer;
transition: background-color 0.2s ease;
}
.toggle-switch.is-on {
background-color: var(--color-brand-primary);
}
.toggle-switch:focus-visible {
outline: max(1px, 0.1em) solid var(--color-brand-dark);
outline-offset: 2px;
}
.toggle-switch__thumb {
width: calc(var(--toggle-height) - (2 * var(--toggle-padding)));
height: calc(var(--toggle-height) - (2 * var(--toggle-padding)));
border-radius: 50%;
background-color: var(--color-surface-white);
transition: transform 0.2s ease;
}
.toggle-switch.is-on .toggle-switch__thumb {
transform: translateX(calc(var(--toggle-width) - var(--toggle-height)));
}
/* The thumb travels the other way in RTL, where the track is mirrored. */
[dir="rtl"] .toggle-switch.is-on .toggle-switch__thumb {
transform: translateX(calc((var(--toggle-width) - var(--toggle-height)) * -1));
}
</style>