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:

assignment v8.8.19 v9.0.0
radio.value = false prop=false, attr=null prop=false, attr="ion-rb-0"
radio.value = true prop=true, attr=null prop="", attr=""
click the radio → group.value true ""

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-group id="group">
  <ion-radio id="r-false">Public</ion-radio>
  <ion-radio id="r-true">Restricted</ion-radio>
</ion-radio-group>
  1. After the components hydrate, run:
document.getElementById('r-false').value = false
document.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