Components
Radio
See also: Form documentation.
Full supportSupported since v105.Full supportSupported since v121.Full supportSupported since v15.4.
Anatomy
<Radio>Container element.
v-modelThe radio input.
v-slot:defaultThe label.
v-slot:end-textSupporting text displayed below the label.
The name prop will get passed down to each radio button in the group.
<script setup lang="ts">import { FieldGroup, FieldLegend, FieldSet, Form, Radio } from "opui-css/vue"</script>
<template> <Form> <FieldSet> <FieldLegend>Legend</FieldLegend> <FieldGroup name="radio-group"> <Radio value="1" checked>Radio 1</Radio> <Radio value="2">Radio 2</Radio> <Radio value="3">Radio 3</Radio> </FieldGroup> </FieldSet> </Form></template><form class="ui-form"> <!--[--> <fieldset class="ui-fieldset"> <!--[--> <legend class=""><!--[-->Legend<!--]--></legend> <div class="ui-field-group" role="group"> <!--[--><label class="ui-radio" ><input type="radio" name="radio-group" value="1" checked /><span class="ui-label" ><!--[-->Radio 1<!--]--></span ><!----></label ><label class="ui-radio" ><input type="radio" name="radio-group" value="2" /><span class="ui-label" ><!--[-->Radio 2<!--]--></span ><!----></label ><label class="ui-radio" ><input type="radio" name="radio-group" value="3" /><span class="ui-label" ><!--[-->Radio 3<!--]--></span ><!----></label ><!--]--> </div> <!--]--> </fieldset> <!--]--></form>Direction
<script setup lang="ts">import { FieldGroup, FieldLegend, FieldSet, Form, Radio } from "opui-css/vue"</script>
<template> <Form> <FieldSet> <FieldLegend>Legend</FieldLegend> <FieldGroup direction="row" name="radio-group-direction"> <Radio value="1" checked>Radio 1</Radio> <Radio value="2">Radio 2</Radio> <Radio value="3">Radio 3</Radio> </FieldGroup> </FieldSet> </Form></template><form class="ui-form"> <!--[--> <fieldset class="ui-fieldset"> <!--[--> <legend class=""><!--[-->Legend<!--]--></legend> <div class="ui-field-group ui-row" role="group"> <!--[--><label class="ui-radio" ><input type="radio" name="radio-group-direction" value="1" checked /><span class="ui-label"><!--[-->Radio 1<!--]--></span><!----></label ><label class="ui-radio" ><input type="radio" name="radio-group-direction" value="2" /><span class="ui-label" ><!--[-->Radio 2<!--]--></span ><!----></label ><label class="ui-radio" ><input type="radio" name="radio-group-direction" value="3" /><span class="ui-label" ><!--[-->Radio 3<!--]--></span ><!----></label ><!--]--> </div> <!--]--> </fieldset> <!--]--></form>Field description
Can be placed above and below the fields.
<script setup lang="ts">import { FieldDescription, FieldGroup, FieldLegend, FieldSet, Form, Radio,} from "opui-css/vue"</script>
<template> <Form> <FieldSet> <FieldLegend>Legend</FieldLegend> <FieldDescription>Field description above fields</FieldDescription> <FieldGroup direction="row" name="radio-group-field-description-1"> <Radio value="1" checked>Radio 1</Radio> <Radio value="2">Radio 2</Radio> <Radio value="3">Radio 3</Radio> </FieldGroup> </FieldSet>
<FieldSet> <FieldLegend>Legend</FieldLegend> <FieldGroup direction="row" name="radio-group-field-description-2"> <Radio value="1" checked>Radio 1</Radio> <Radio value="2">Radio 2</Radio> <Radio value="3">Radio 3</Radio> </FieldGroup> <FieldDescription>Field description below fields</FieldDescription> </FieldSet> </Form></template><form class="ui-form"> <!--[--> <fieldset class="ui-fieldset"> <!--[--> <legend class=""><!--[-->Legend<!--]--></legend> <p class="ui-field-description"> <!--[-->Field description above fields<!--]--> </p> <div class="ui-field-group ui-row" role="group"> <!--[--><label class="ui-radio" ><input type="radio" name="radio-group-field-description-1" value="1" checked /><span class="ui-label"><!--[-->Radio 1<!--]--></span><!----></label ><label class="ui-radio" ><input type="radio" name="radio-group-field-description-1" value="2" /><span class="ui-label"><!--[-->Radio 2<!--]--></span><!----></label ><label class="ui-radio" ><input type="radio" name="radio-group-field-description-1" value="3" /><span class="ui-label"><!--[-->Radio 3<!--]--></span><!----></label ><!--]--> </div> <!--]--> </fieldset> <fieldset class="ui-fieldset"> <!--[--> <legend class=""><!--[-->Legend<!--]--></legend> <div class="ui-field-group ui-row" role="group"> <!--[--><label class="ui-radio" ><input type="radio" name="radio-group-field-description-2" value="1" checked /><span class="ui-label"><!--[-->Radio 1<!--]--></span><!----></label ><label class="ui-radio" ><input type="radio" name="radio-group-field-description-2" value="2" /><span class="ui-label"><!--[-->Radio 2<!--]--></span><!----></label ><label class="ui-radio" ><input type="radio" name="radio-group-field-description-2" value="3" /><span class="ui-label"><!--[-->Radio 3<!--]--></span><!----></label ><!--]--> </div> <p class="ui-field-description"> <!--[-->Field description below fields<!--]--> </p> <!--]--> </fieldset> <!--]--></form>Disabled
Attach the disabled attribute to the <fieldset> element.
<script setup lang="ts">import { FieldGroup, FieldLegend, FieldSet, Form, Radio } from "opui-css/vue"</script>
<template> <Form> <FieldSet disabled> <FieldLegend>Legend</FieldLegend> <FieldGroup direction="row" name="radio-group-disabled"> <Radio value="1" checked>Radio 1</Radio> <Radio value="2">Radio 2</Radio> <Radio value="3">Radio 3</Radio> </FieldGroup> </FieldSet> </Form></template><form class="ui-form"> <!--[--> <fieldset class="ui-fieldset" disabled> <!--[--> <legend class=""><!--[-->Legend<!--]--></legend> <div class="ui-field-group ui-row" role="group"> <!--[--><label class="ui-radio" ><input type="radio" name="radio-group-disabled" value="1" checked /><span class="ui-label"><!--[-->Radio 1<!--]--></span><!----></label ><label class="ui-radio" ><input type="radio" name="radio-group-disabled" value="2" /><span class="ui-label" ><!--[-->Radio 2<!--]--></span ><!----></label ><label class="ui-radio" ><input type="radio" name="radio-group-disabled" value="3" /><span class="ui-label" ><!--[-->Radio 3<!--]--></span ><!----></label ><!--]--> </div> <!--]--> </fieldset> <!--]--></form>Required
Add the required attribute on at least one Radio. It is forwarded to the underlying <input>.
<script setup lang="ts">import { FieldGroup, FieldLegend, FieldSet, Form, Radio } from "opui-css/vue"</script>
<template> <Form> <FieldSet> <FieldLegend>These are required!</FieldLegend> <FieldGroup direction="row" name="radio-group-required"> <Radio value="1" required>Radio 1</Radio> <Radio value="2" required>Radio 2</Radio> <Radio value="3" required>Radio 3</Radio> </FieldGroup> </FieldSet> </Form></template><form class="ui-form"> <!--[--> <fieldset class="ui-fieldset"> <!--[--> <legend class=""><!--[-->These are required!<!--]--></legend> <div class="ui-field-group ui-row" role="group"> <!--[--><label class="ui-radio" ><input type="radio" name="radio-group-required" value="1" required /><span class="ui-label"><!--[-->Radio 1<!--]--></span><!----></label ><label class="ui-radio" ><input type="radio" name="radio-group-required" value="2" required /><span class="ui-label"><!--[-->Radio 2<!--]--></span><!----></label ><label class="ui-radio" ><input type="radio" name="radio-group-required" value="3" required /><span class="ui-label"><!--[-->Radio 3<!--]--></span><!----></label ><!--]--> </div> <!--]--> </fieldset> <!--]--></form>Validation
Attach data-invalid to the FieldSet wrapper, or use the error prop on individual Radio components.
<script setup lang="ts">import { FieldGroup, FieldLegend, FieldSet, Form, Radio } from "opui-css/vue"</script>
<template> <Form> <FieldSet data-invalid> <FieldLegend>Legend</FieldLegend> <FieldGroup direction="row" name="radio-group-validation"> <Radio value="1" checked>Radio 1</Radio> <Radio value="2">Radio 2</Radio> <Radio value="3">Radio 3</Radio> </FieldGroup> <span class="ui-end-text">Something went wrong!</span> </FieldSet> </Form></template><form class="ui-form"> <!--[--> <fieldset class="ui-fieldset" data-invalid> <!--[--> <legend class=""><!--[-->Legend<!--]--></legend> <div class="ui-field-group ui-row" role="group"> <!--[--><label class="ui-radio" ><input type="radio" name="radio-group-validation" value="1" checked /><span class="ui-label"><!--[-->Radio 1<!--]--></span><!----></label ><label class="ui-radio" ><input type="radio" name="radio-group-validation" value="2" /><span class="ui-label" ><!--[-->Radio 2<!--]--></span ><!----></label ><label class="ui-radio" ><input type="radio" name="radio-group-validation" value="3" /><span class="ui-label" ><!--[-->Radio 3<!--]--></span ><!----></label ><!--]--> </div> <span class="ui-end-text">Something went wrong!</span><!--]--> </fieldset> <!--]--></form>API
Radio API
| Prop | Type | Default | Description |
|---|---|---|---|
error | boolean | false | Shows error styles. |
hideLabel | boolean | false | Visually hides the label. |
size | "small", "large" | - | The size of the element. |
stack | boolean | false | Stacks the label under the input. |
v-model | string, number, boolean | - | The selected value of the group. |
Slots
| Slot | Description |
|---|---|
default | The label. |
end-text | Supporting text displayed below the label. |
Attributes that aren't props, such as disabled, name or value, go to the <input>.
Field group API
| Prop | Type | Default | Description |
|---|---|---|---|
direction | "row", "column" | - | The orientation of the element. |
name | string | - | Sets name on every input, select and textarea inside. |
Slots
| Slot | Description |
|---|---|
default | The fields, such as checkboxes, radios or switches. |
Browser support
Full supportSupported since v105.Full supportSupported since v121.Full supportSupported since v15.4.
See also the full browser support guide.
Installation
See also
@layer components.root { label.ui-radio { --_input-size: 1.125rem; --_indicator-size: 50%;
align-items: center; color: var(--text-primary); cursor: pointer; display: inline-grid; gap: 0 var(--size-2); grid-auto-columns: auto; grid-auto-flow: column; inline-size: fit-content; line-height: 1.5; transform: translateZ(0); user-select: none;
/* Disabled */ &:has([disabled]) { cursor: not-allowed; opacity: 0.64; user-select: none;
input { cursor: not-allowed; } }
/* Required dot */ &:has(:invalid) { .ui-label:after { color: var(--red); content: "*"; inset: 0 -0.25ex auto auto; position: absolute; } }
/* Label */ .ui-label { color: var(--text-primary); font-size: var(--font-size-05); grid-column: 2; grid-row: 1; padding-inline: 0 1ex; position: relative; }
/* End text */ :where(.ui-end-text) { color: var(--text-muted); font-size: var(--font-size-0); grid-column: 2; grid-row: 2; line-height: 1.5; z-index: 1; }
/* Stacked layout */ &.ui-stack { grid-auto-columns: unset; justify-items: center;
.ui-label { grid-column: 1/-1; grid-row: 2; margin-block-start: var(--size-1); padding-inline: 1ex;
/* Required dot */ &::after { inset: 0 -0.25ex auto auto; } }
.ui-end-text { grid-column: 1/-1; grid-row: 3; } }
/* Input */ input[type="radio"] { appearance: none; aspect-ratio: 1; background-color: var(--surface-default); block-size: var(--_input-size); border: 1px solid var(--field-border-color); border-radius: var(--radius-round); box-sizing: border-box; cursor: pointer; display: grid; inline-size: var(--_input-size); margin: 0; padding: 0; place-items: center; position: relative;
&:checked { background-color: var(--primary); border-color: var(--primary); }
/* Dot */ &::after { background-color: var(--primary-contrast); block-size: var(--_indicator-size); border-radius: var(--radius-round); content: ""; inline-size: var(--_indicator-size); margin: auto; opacity: 0; }
&:checked::after { opacity: 1; }
&::before { --highlight-size: 175%; } }
/* Sizes */ &.ui-small { input[type="radio"] { --_input-size: var(--size-3); } }
&.ui-large { input[type="radio"] { --_input-size: var(--size-4); } }
/* Validation */ &[data-invalid], &:has(:user-invalid) { --primary: var(--critical);
:where(.ui-end-text) { color: var(--critical); } }
/* Touch devices */ @media (pointer: coarse) { input[type="radio"] { block-size: var(--size-4); inline-size: var(--size-4); } }
@media (forced-colors: active) { input[type="radio"] { border: 1px solid CanvasText;
&:checked { background-color: SelectedItem; border-color: SelectedItem;
&::after { background-color: SelectedItemText; } } } } }}@layer components.extended { /* Form */ :where(.ui-form) { display: grid; gap: var(--size-8);
hr { margin-block: 0; } }
/* Fieldset */ :where(.ui-fieldset) { all: unset; border: 0; border-radius: 0; display: grid; gap: var(--size-1); margin: 0; min-inline-size: 0; padding: 0;
/* Legend */ :where(legend, .ui-legend) { all: unset; color: var(--text-primary); font-weight: 600; margin-block-end: var(--size-3); padding: 0;
&:has(+ :where(.ui-field-description)) { margin-block-end: 0; } }
/* Field description / supporting text */ :where(.ui-field-description) { color: var(--text-muted); font-size: var(--font-size-05); line-height: var(--font-lineheight-3);
&:has(+ *) { margin-block-end: var(--size-3); } }
/* End text */ :where(.ui-end-text) { color: var(--text-muted); font-size: var(--font-size-0); line-height: var(--font-lineheight-3); }
&:has(.ui-text-field.ui-row) { row-gap: var(--size-7); }
/* Disabled */ &[disabled] { opacity: 0.64; user-select: none;
input, label, .ui-label { cursor: not-allowed; } }
/* Error / validation */ &[data-invalid] { :where(.ui-end-text) { color: var(--critical); }
:where(.ui-checkbox, .ui-radio, .ui-switch) { --primary: var(--critical);
:where(.ui-end-text) { color: var(--critical); } }
:where(.ui-switch) { input { border-radius: var(--radius-round); outline: 2px solid var(--critical); } } }
/* Required */ &:has(:invalid) { :where(legend, .ui-legend) { padding-inline-end: 1ex; position: relative;
&::after { color: var(--red); content: "*"; inset: 0 -0.25ex auto auto; position: absolute; } } } }
/* Field group */ :where(.ui-field-group) { display: flex; flex-direction: column; gap: var(--size-4);
/* If it only has checkboxes, radios, or switches */ &:has(> :where(.ui-checkbox, .ui-radio, .ui-switch)):not( :has(> :not(.ui-checkbox, .ui-radio, .ui-switch)) ) { gap: var(--size-2); }
/* If it only has buttons */ &:has(.ui-button):not(:has(*:not(.ui-button))) { align-items: center; flex-direction: row; gap: var(--size-2);
/* If it's not preceded by an hr */ &:not(hr + &) { margin-block-start: var(--size-4); } }
& + & { margin-block-start: var(--size-5); }
/* Directions */ &.ui-row { flex-direction: row; flex-wrap: wrap;
&:has(> :where(.ui-checkbox, .ui-radio, .ui-switch)) { gap: var(--size-4); } } }}