Skip to content

@headlessui/react@2.2.10 label for attribute is incorrectly applied for radio buttons #3883

Description

@fischi20

TLDR:

Labels with the for attribute according to spec should not reference a span, it does work, but it is not HTML spec compliant, which means it is technically not correct and that there is no guarantee for it's stability.


Labels linked with Radio buttons add the for attribute to the label, the for attribute is only allowed for labelable elements, where the span element does not fall under. The functionality of proxying events is correct and desireable. As of the current implementation it works functionally, but it is not spec compliant. As of my understanding the solutions would mostly push to either a custom web component or an invisible native input type radio using appearance:none which is supported by all major Browsers since over a decade, where I think realistically, only the 2. option would fit this project.

import { Field, Label, Radio, RadioGroup } from '@headlessui/react'
import { useState } from 'react'

const plans = [
  { name: 'Startup', available: true },
  { name: 'Business', available: true },
  { name: 'Enterprise', available: false },
]

export default function Example() {
  let [selected, setSelected] = useState(plans[0])

  return (
    <RadioGroup value={selected} onChange={setSelected} aria-label="Server size">
      {plans.map((plan) => (
        <Field key={plan.name} disabled={!plan.available} className="flex items-center gap-2">
          <Radio
            value={plan}
            className="group flex size-5 items-center justify-center rounded-full border bg-white data-checked:bg-blue-400 data-disabled:bg-gray-100"
          >
            <span className="invisible size-2 rounded-full bg-white group-data-checked:visible" />
          </Radio>
          <Label className="data-disabled:opacity-50">{plan.name}</Label>
        </Field>
      ))}
    </RadioGroup>
  )
}

sourced from https://headlessui.com/react/radio-group#binding-objects-as-values

In this example the outputted label will look something like:

<label class="data-disabled:opacity-50" id="headlessui-label-_r_3_" for="headlessui-control-_r_1_" data-headlessui-state="">Startup</label>

Is this a concern headlessui would care about or should I recreate the radio group myself in order to be HTML spec compliant? This error was noticed due to the Google Chrome Developer Tools flagging it (It does not always flag it, there are some precise parsing requirements, which is why I did not manage to reproduce it in an online playground. It also does not get flagged if the referenced element by the for attribute appears after the Label element. This are not fixes tho, they are essentially just bypassing the linter.

For reference, I can provide the code emitting that structure in my current codebase, even tho it is essentially the same as the example from the docs above

import { Field, Radio as HeadlessRadio, Label } from "@headlessui/react";
import styles from "./Radio.module.css";

interface RadioProps {
  label: string;
  value: string | number;
  className?: string;
}

const Radio = ({ label, value, className, ...rest }: RadioProps) => {

  return (
  <Field className={className}>
    <HeadlessRadio {...rest} className={styles.radio} value={value}/>
    <Label
    className={styles.label}
    >
      {label}
    </Label>
  </Field>
)};

export default Radio;

and the generated HTML from it

<div data-headlessui-state=""><span class="_radio_173cg_7" id="headlessui-control-_r_27_" role="radio" aria-checked="false" tabindex="-1" data-headlessui-state="" aria-labelledby="headlessui-label-_r_29_"></span><label class="_label_173cg_1" id="headlessui-label-_r_29_" for="headlessui-control-_r_27_" data-headlessui-state="">Ja, ich bin geschäftlich unterwegs</label><span hidden="" style="position: fixed; top: 1px; left: 1px; width: 1px; height: 0px; padding: 0px; margin: -1px; overflow: hidden; clip: rect(0px, 0px, 0px, 0px); white-space: nowrap; border-width: 0px; display: none;"></span></div>
Image

In the first place I would be mostly interested in knowing if this is something that the library would solve itself or if I should take care of it.

If needed I can implement the desired solution to this issue if any changes are wanted, I would just rather first ask since the changes wuould change defaults on a project that is not mine.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions