Appearance
Radio 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: 'sm', // xs, sm, md, lg, xl
},
disabled: {
type: Boolean,
default: false,
},
readonly: {
type: Boolean,
default: false,
},
toggle: {
type: Boolean,
default: false,
},
label: {
type: String,
default: '',
},
value: {
type: [String, Number, Boolean],
required: true,
},
multiple: {
type: Boolean,
default: false,
},
modelValue: {
type: [String, Number, Boolean, Array],
required: false,
default: (props) => (props.multiple ? [] : ''),
validator: (value, props) => {
if (props.multiple) {
return Array.isArray(value);
}
return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
},
},
selected: {
type: [String, Number, Boolean, Array],
required: false,
default: (props) => (props.multiple ? [] : ''),
validator: (value, props) => {
if (props.multiple) {
return Array.isArray(value);
}
return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
},
},
valueComparator: {
type: Function,
default: (a, b, multiple) => {
if (multiple) return Array.isArray(a) && a.includes(b);
return a === b;
},
},
variant: {
type: String,
default: 'default',
},
});
const {
size,
disabled,
readonly,
label,
modelValue,
selected,
valueComparator,
multiple,
value: inputValue,
toggle,
variant,
} = toRefs(props);
const internalValue = computed(() => {
let val = modelValue.value || selected.value;
if (multiple.value) {
val = modelValue.value?.length ? modelValue.value : selected.value;
}
return multiple.value ? (Array.isArray(val) ? val : []) : val || '';
});
// Determine if this radio should be selected
const isChecked = computed(() => {
if (internalValue.value === '' && inputValue.value === '') return false;
return valueComparator.value(internalValue.value, inputValue.value, multiple.value);
});
// Handle click
function handleClick(event) {
// Skip if disabled or readonly
if (disabled.value || readonly.value) return;
// Handle multiple selection mode
if (multiple.value) {
const isSelected = isChecked.value;
const currentValues = Array.isArray(internalValue.value) ? internalValue.value : [];
let newValue;
if (isSelected) {
// Only allow removing all options if toggle is true
if (toggle.value || currentValues.length > 1) {
newValue = currentValues.filter((v) => v !== inputValue.value);
} else {
// Keep the current value if it's the only one and toggle is false
newValue = currentValues;
}
} else {
newValue = [...currentValues, inputValue.value];
}
emit('update:modelValue', newValue);
emit('change', newValue, inputValue.value, event);
}
// Handle single selection mode
else {
const isSelected = isChecked.value;
const newValue = toggle.value && isSelected ? '' : inputValue.value;
emit('update:modelValue', newValue);
emit('change', newValue, inputValue.value, event);
console.log(newValue);
}
}
</script>
<template>
<label class="radio-container" :class="[size, variant, { disabled, readonly }]">
<input hidden type="radio" :value="inputValue" :checked="isChecked" :disabled="disabled" @click="handleClick" />
<slot name="icon" :props="{ isChecked }">
<div class="radio">
<div class="inner-circle"></div>
</div>
</slot>
<slot name="label" :props="{ isChecked }">{{ label }}</slot>
</label>
</template>
<style scoped lang="scss">
.radio-container {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
user-select: none;
color: #000;
position: relative;
*,
*::before,
*::after {
box-sizing: border-box;
}
input {
display: none;
&:checked + .radio .inner-circle {
transform: translate(-50%, -50%) scale(1);
}
}
&.disabled {
pointer-events: none;
opacity: 0.5;
}
&.readonly {
pointer-events: none;
}
.radio {
position: relative;
display: inline-block;
border: 0.0625rem solid;
border-radius: 50%;
.inner-circle {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 60%;
height: 60%;
border-radius: 50%;
transform: translate(-50%, -50%) scale(0);
transition: transform 0.2s ease-in-out;
}
&:hover .inner-circle {
transform: translate(-50%, -50%) scale(1);
}
&:active {
opacity: 0.7;
}
}
// Sizes
&.xs {
font-size: 0.625rem;
.radio {
width: 0.75rem;
height: 0.75rem;
}
}
&.sm {
font-size: 0.75rem;
.radio {
width: 0.875rem;
height: 0.875rem;
}
}
&.md {
font-size: 0.875rem;
.radio {
width: 1rem;
height: 1rem;
}
}
&.lg {
font-size: 1rem;
.radio {
width: 1.125rem;
height: 1.125rem;
}
}
&.xl {
font-size: 1.125rem;
.radio {
width: 1.25rem;
height: 1.25rem;
}
}
// Variants
&.default {
color: #000;
.radio {
border-color: #000;
.inner-circle {
background-color: #000;
}
}
}
&.success {
color: #22c55e;
.radio {
border-color: #22c55e;
.inner-circle {
background-color: #22c55e;
}
}
}
}
</style>