base-ui icon indicating copy to clipboard operation
base-ui copied to clipboard

[radio] Allow controlling the render of the hidden `input` as well, not just the `button`

Open benface opened this issue 2 months ago • 8 comments

Feature request

Summary

When using Radio.Root's render prop, whatever we return only replaces the button that Radio.Root normally renders, but there's no way to control where/how to render the hidden input that it renders beside it. I would expect that level of control from a headless component.

Examples in other libraries

Not sure.

Motivation

I would like to wrap the button and input that Radio.Root renders in a div or something so that Tailwind classes starting with last: or only: work as expected (they don't because the button which receives the class is never the last or only child, due to the input that gets rendered beside it). Of course, I could wrap Radio.Root itself and set the last:/only: classes on the wrapper div, but in my case, I actually want to set the classes conditionally on state that is only available in Radio.Root's render (namely checked)...

Thank you for considering this. 🙏

benface avatar Nov 05 '25 17:11 benface

@benface Are you talking about the input that's a sibling of the button role="radio" or the one that's a sibling of the div role="radiogroup"? You just reminded me that the sibling-of-button is redundant, as we don't intend to support a standalone group-less <Radio> 😅

mj12albert avatar Nov 06 '25 04:11 mj12albert

The sibling of the button role="radio"! Wow sorry @mj12albert I just noticed that I called it RadioGroup 5 times in the original post, just changed to Radio.Root. 😅

benface avatar Nov 06 '25 05:11 benface

For Radio, removing the redundant input should solve the issue

But I think Checkbox has the same issue and it's sibling-to-button input isn't redundant, in which case the only "workaround" is to use Field.Item or an implicit label and set first/last: on that 🤔

mj12albert avatar Nov 06 '25 07:11 mj12albert

Yes, please consider all components that use this pattern; I guess I haven't been bitten by Checkbox yet but it's probably a matter of time.

Does Field.Item have access the checked state of the checkbox? If not, it's not a working workaround for the problem I described with Radio.

benface avatar Nov 06 '25 12:11 benface

wrap the button and input that Radio.Root renders in a div or something so that Tailwind classes starting with last: or only: work as expected

Is this what you're trying to do?https://stackblitz.com/edit/vm4oh1kt?file=src%2FApp.tsx

I forgot Field.Item isn't released yet, but this TW class would work for a plain div though it's a bit long: first:has-[[data-checked]]:bg-[#ff000088] last:has-[[data-checked]]:bg-[#0000ff88]

mj12albert avatar Nov 06 '25 18:11 mj12albert

Thank you @mj12albert for taking the time to do that. To be perfectly honest with you, I simplified the problem a lot for the "Motivation" section; I'm working on a design system / component library that uses Base UI and I want my Radio component to support Tailwind classes like e.g. last:checked:variant-primary (where the checked variant is customized to include data-checked and variant-primary is a custom Tailwind utility that changes the style of a component), which would be possible if I could have my Radio render a Radio.Root with something like render={(buttonProps, { checked, Input }) => <div data-checked={checked}><button {...buttonProps} /><Input /></div>}. Unfortunately, having a Field.Item wrapper as the root element of my Radio component doesn't seem like the right solution, because it doesn't know the checked state of the Radio.Root it contains, so it can't have the data-checked attribute that a checked: class would look for (and the root element is always the one that receives className in my library, by convention and to properly support classes like hidden, absolute, order-first, etc.).

benface avatar Nov 06 '25 19:11 benface

@benface Thinking about this more - for Checkbox, if you could control rendering of the input, where would you render it? It could only be either a sibling or child of the role="checkbox", and making it a child would be invalid HTML since the checkbox is a button (not sure if a span button would work)

mj12albert avatar Nov 12 '25 04:11 mj12albert

@mj12albert I would do this:

render=((buttonProps, inputProps, state) => (
  <div>
    <button {...buttonProps} />
    <input {...inputProps} />
  </div>
)}

benface avatar Nov 12 '25 12:11 benface