Skip to content

bug: ion-radio reflects value to the attribute, destroying non-string values #31394

Description

@maximilianschmid

Ionic Framework Version

  • v9.x

(Not present in v8.8.19.)

Current Behavior

ion-radio's value property is now reflected to the value attribute. Because an attribute is a string, any non-string value is round-tripped through the attribute and destroyed:

assignmentv8.8.19v9.0.0
radio.value = falseprop=false, attr=nullprop=false, attr="ion-rb-0"
radio.value = trueprop=true, attr=nullprop="", attr=""
click the radio → group.valuetrue""

A boolean true reflects as the empty attribute value="" (boolean attribute semantics), and the attribute-changed callback then parses it back into the property as "". The radio still renders as visually checked, so the UI looks correct while ion-radio-group's value — and therefore any framework binding — is silently wrong.

This also conflicts with the documented compareWith property, which exists specifically so that ion-radio values can be objects.

Expected Behavior

value should keep whatever value it was assigned, as in v8. Reflecting an any-typed property to an attribute is lossy and should not happen.

Steps to Reproduce

  1. ionic start radioValue blank --type=vue (framework is irrelevant — this reproduces on bare web components).
  2. Install @ionic/core@9.0.0.
  3. Render:
<ion-radio-groupid="group"><ion-radioid="r-false">Public</ion-radio><ion-radioid="r-true">Restricted</ion-radio></ion-radio-group>
  1. After the components hydrate, run:
document.getElementById('r-false').value=falsedocument.getElementById('r-true').value=true// v8: true / null v9: "" / ""console.log(document.getElementById('r-true').value,document.getElementById('r-true').getAttribute('value'))
  1. Click the second radio and read document.getElementById('group').value.

Actual (v9):""
Expected:true

Code Reproduction URL

No Stackblitz link, sorry — but the reproduction above needs no framework at all: two <ion-radio> elements, two property assignments and a console.log. It reproduces on bare @ionic/core and flips purely on the version. Happy to put it on Stackblitz if that would help triage.

Ionic Info

Ionic:
Ionic CLI : 7.2.1
Ionic Framework : @ionic/vue 9.0.0
Capacitor:
Capacitor CLI : 8.5.0
@capacitor/android : 8.5.0
@capacitor/core : 8.5.0
@capacitor/ios : 8.5.0
System:
NodeJS : v26.7.0
npm : 11.19.0
OS : macOS

Additional Information

Cause

The Stencil member flags for ion-radio's value changed between versions (@ionic/core/components, minified proxy metadata):

-"ion-radio",{color:[513],name:[1],disabled:[4],value:[8],labelPlacement:[1,"label-placement"],…}+"ion-radio",{color:[513],name:[1],disabled:[4],value:[520],labelPlacement:[1,"label-placement"],…}

8 is MEMBER_FLAGS.Any; 520 is 512 | 8, i.e. ReflectAttr | Any. So @Prop() on value gained reflect: true.

The interaction with connectedCallback makes the false case odd too:

connectedCallback(){if(this.value===undefined){this.value=this.inputId}}

which is why a falsy value leaves the generated ion-rb-N id in the reflected attribute.

I scanned every component's proxy metadata across 8.8.19 → 9.0.0: ion-radio.value is the only prop that gained the reflect bit, so reverting it should be low-risk.

Suggested fix

Drop reflect: true from ion-radio's value prop (restore value:[8]). We worked around it by giving our radios string values, which is arguably the more idiomatic choice anyway — but the reflect is still lossy for anyone relying on boolean, number or compareWith object values.

Impact

Any ion-radio with a non-string value breaks silently — the radio appears selected while the bound model receives "". In our app this hid a whole conditional section (v-if on a boolean bound to the radio group) and failed 3 E2E tests, with a misleading symptom: the element that should have appeared "was never found".

Found while upgrading one app from 8.8.19 to 9.0.0; two more regressions from the same release: #31392 and #31393, both in @ionic/vue's class syncing.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions