Skip to content

Radio Group

INFO

This page has not yet been updated to use the defineModel macro, which was added in Vue 3.4. The techniques described here should still work, but in some cases it might be better to use defineModel instead.

Radio Group Example

A radio group component acts as a wrapper around multiple radio components, with a single v-model on the wrapper:

vue
<template>
  <basic-radio-group v-model="radioValue">
    <basic-radio value="First" />
    <basic-radio value="Second" />
    <basic-radio value="Third" />
  </basic-radio-group>
  <pre>Bound value: {{ radioValue }}</pre>
</template>

<script setup>
import { ref } from 'vue'
import BasicRadio from './radio.vue'
import BasicRadioGroup from './radio-group.vue'

const radioValue = ref('First')
</script>

It would look something like this:

Live Example
Bound value: First

SFC Playground

radio-group.vue would use provide to communicate with radio.vue:

vue
<template>
  <div>
    <slot />
  </div>
</template>

<script setup>
import { computed, provide } from 'vue'

const props = defineProps({
  modelValue: {
    required: true
  }
})

const emit = defineEmits(['update:modelValue'])

provide('radioModel', computed({
  get () {
    return props.modelValue
  },
  set (value) {
    emit('update:modelValue', value)
  }
}))
</script>

The radio.vue here is specially written to use inject instead of a modelValue:

vue
<template>
  <div>
    <input type="radio" v-model="radioModel" :value="value">
  </div>
</template>

<script setup>
import { inject } from 'vue'

const props = defineProps({
  value: {
    required: true
  }
})

const radioModel = inject('radioModel')
</script>

Support for direct use of v-model with the radio component has been omitted from this example. That technique is shown in the Radio example. The two can be combined by using a default value with inject and then checking whether that value is set.

Vue Patterns

See Coupled Components with provide/inject

Apart from the naming choices, there's nothing in this radio-group.vue implementation that assumes the children are radio buttons. It could be made to work with checkboxes, toggle switches or any similar components that involve picking from a list of options.

It could be argued that injecting a computed like this is violating one-way data flow for updates, or at least it gives that impression from the perspective of radio.vue. You could inject a separate ref and update function if you prefer.

Alternatives

Another way to implement a radio group would be to use a prop to pass the options rather than a slot. e.g.:

vue
<template>
  <basic-radio-group
    v-model="radioValue"
    :options="['First', 'Second', 'Third']"
  />
</template>

As the radio group is now responsible for creating the child components, it can use props and events for communicating with the children rather than provide/inject. This does simplify the implementation of the children, but it also forces the radio group to take on responsibility for laying out the radios. Whereas a slot allows other containers, dividers and text to be included along with the radios, the prop-based approach needs everything to be passed in via props. The slot-based approach could be seen as a better separation of concerns, though if you don't need the extra flexibility it may be unnecessary complexity.

Both approaches could be implemented in the same component.

Libraries

Various libraries include a radio component, including: