Radio Button
A set of checkable buttons, known as radio buttons, where no more than one can be checked at a time.
Usage
Example
Radios with the same name: form one group. RadioGroup adds role="radiogroup"; name it with aria: {label:} or aria: {labelledby:}.
RadioGroup(class: "w-fit", aria: {label: "Density"}) do
div(class: "flex items-center gap-3") do
RadioButton(id: "density_default", name: "density", value: "default")
FormFieldLabel(for: "density_default") { "Default" }
end
div(class: "flex items-center gap-3") do
RadioButton(id: "density_comfortable", name: "density", value: "comfortable", checked: true)
FormFieldLabel(for: "density_comfortable") { "Comfortable" }
end
div(class: "flex items-center gap-3") do
RadioButton(id: "density_compact", name: "density", value: "compact")
FormFieldLabel(for: "density_compact") { "Compact" }
end
end
Disabled
RadioGroup(class: "w-fit", aria: {label: "Plan"}) do
div(class: "flex items-center gap-3") do
RadioButton(id: "plan_free", name: "plan_disabled", value: "free", checked: true)
FormFieldLabel(for: "plan_free") { "Free" }
end
div(class: "flex items-center gap-3") do
RadioButton(id: "plan_enterprise", name: "plan_disabled", value: "enterprise", disabled: true)
FormFieldLabel(for: "plan_enterprise") { "Enterprise" }
end
end
Invalid
RadioGroup(class: "w-fit", aria: {label: "Notifications"}) do
div(class: "flex items-center gap-3") do
RadioButton(id: "notify_all", name: "notify_invalid", value: "all", aria: {invalid: "true"})
FormFieldLabel(for: "notify_all") { "All new messages" }
end
div(class: "flex items-center gap-3") do
RadioButton(id: "notify_none", name: "notify_invalid", value: "none", aria: {invalid: "true"})
FormFieldLabel(for: "notify_none") { "Nothing" }
end
end
Required
required: on each radio makes the browser ask for one of them. FormField shows the message and marks the radios invalid.
form(class: "flex w-fit flex-col gap-4") do
FormFieldSet do
FormFieldLegend(id: "notify_legend", variant: :label) { "Notify me about" }
FormField do
RadioGroup(aria: {labelledby: "notify_legend"}) do
div(class: "flex items-center gap-3") do
RadioButton(id: "notify_mentions", name: "notify", value: "mentions", required: true)
FormFieldLabel(for: "notify_mentions") { "Direct messages and mentions" }
end
div(class: "flex items-center gap-3") do
RadioButton(id: "notify_nothing", name: "notify", value: "nothing", required: true)
FormFieldLabel(for: "notify_nothing") { "Nothing" }
end
end
FormFieldError()
end
end
Button(type: "submit", class: "w-fit") { "Save" }
end
Installation
Using RubyUI CLI
Run the install command
rails g ruby_ui:component RadioButton
Manual installation
1
Add RubyUI::RadioButton to app/components/ruby_ui/radio_button/radio_button.rb
# frozen_string_literal: true
module RubyUI
# A radio, styled after shadcn's RadioGroupItem (Base UI + vega). Renders a
# native <input type="radio"> instead of Base UI's span[role=radio] plus
# hidden input, so arrow keys, roving focus, required and form submission
# come from the browser. The dot is the input's ::before (size-2 in
# text-primary-foreground) and ::after widens the hit area, so there is no
# wrapper and peer-* classes on a following label keep working.
#
# Radios group by their name attribute. Wrap them in RadioGroup for role="radiogroup".
class RadioButton < Base
BASE = [
"peer relative aspect-square size-4 shrink-0 appearance-none rounded-full border border-input bg-transparent p-0",
# High contrast: give the drawing back to the browser (as @tailwindcss/forms does).
"forced-colors:appearance-auto forced-colors:checked:before:hidden",
"text-inherit outline-none dark:bg-input/30",
"after:absolute after:-inset-x-3 after:-inset-y-2",
"before:pointer-events-none before:absolute before:top-1/2 before:left-1/2 before:hidden before:size-2",
"before:-translate-x-1/2 before:-translate-y-1/2 before:rounded-full before:bg-current",
"focus:ring-0 focus:ring-offset-0 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50",
"disabled:cursor-not-allowed disabled:opacity-50",
"aria-disabled:cursor-not-allowed aria-disabled:opacity-50 aria-disabled:pointer-events-none",
"aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20",
"dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
"group-has-[:focus-visible]/field-label:ring-0 group-has-[:focus-visible]/field-label:not-checked:border-input",
# Checked: the dot.
"checked:border-primary checked:bg-primary checked:bg-none checked:text-primary-foreground",
"dark:checked:bg-primary aria-invalid:checked:border-primary checked:before:block",
"group-has-[:focus-visible]/field-label:checked:border-primary"
].freeze
def view_template
input(**attrs)
end
private
def default_attrs
{
type: "radio",
data: {
slot: "radio-group-item",
ruby_ui__form_field_target: "input",
action: "change->ruby-ui--form-field#onInput invalid->ruby-ui--form-field#onInvalid"
},
class: BASE
}
end
end
end
2
Add RubyUI::RadioButtonDocs to app/components/ruby_ui/radio_button/radio_button_docs.rb
# frozen_string_literal: true
class Views::Docs::RadioButton < Views::Base
def view_template
component = "RadioButton"
div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do
render Docs::Header.new(title: "Radio Button", description: "A set of checkable buttons, known as radio buttons, where no more than one can be checked at a time.")
Heading(level: 2) { "Usage" }
render Docs::VisualCodeExample.new(title: "Example", description: "Radios with the same name: form one group. RadioGroup adds role=\"radiogroup\"; name it with aria: {label:} or aria: {labelledby:}.", context: self) do
<<~RUBY
RadioGroup(class: "w-fit", aria: {label: "Density"}) do
div(class: "flex items-center gap-3") do
RadioButton(id: "density_default", name: "density", value: "default")
FormFieldLabel(for: "density_default") { "Default" }
end
div(class: "flex items-center gap-3") do
RadioButton(id: "density_comfortable", name: "density", value: "comfortable", checked: true)
FormFieldLabel(for: "density_comfortable") { "Comfortable" }
end
div(class: "flex items-center gap-3") do
RadioButton(id: "density_compact", name: "density", value: "compact")
FormFieldLabel(for: "density_compact") { "Compact" }
end
end
RUBY
end
render Docs::VisualCodeExample.new(title: "Disabled", context: self) do
<<~RUBY
RadioGroup(class: "w-fit", aria: {label: "Plan"}) do
div(class: "flex items-center gap-3") do
RadioButton(id: "plan_free", name: "plan_disabled", value: "free", checked: true)
FormFieldLabel(for: "plan_free") { "Free" }
end
div(class: "flex items-center gap-3") do
RadioButton(id: "plan_enterprise", name: "plan_disabled", value: "enterprise", disabled: true)
FormFieldLabel(for: "plan_enterprise") { "Enterprise" }
end
end
RUBY
end
render Docs::VisualCodeExample.new(title: "Invalid", context: self) do
<<~RUBY
RadioGroup(class: "w-fit", aria: {label: "Notifications"}) do
div(class: "flex items-center gap-3") do
RadioButton(id: "notify_all", name: "notify_invalid", value: "all", aria: {invalid: "true"})
FormFieldLabel(for: "notify_all") { "All new messages" }
end
div(class: "flex items-center gap-3") do
RadioButton(id: "notify_none", name: "notify_invalid", value: "none", aria: {invalid: "true"})
FormFieldLabel(for: "notify_none") { "Nothing" }
end
end
RUBY
end
render Docs::VisualCodeExample.new(title: "Required", description: "required: on each radio makes the browser ask for one of them. FormField shows the message and marks the radios invalid.", context: self) do
<<~RUBY
form(class: "flex w-fit flex-col gap-4") do
FormFieldSet do
FormFieldLegend(id: "notify_legend", variant: :label) { "Notify me about" }
FormField do
RadioGroup(aria: {labelledby: "notify_legend"}) do
div(class: "flex items-center gap-3") do
RadioButton(id: "notify_mentions", name: "notify", value: "mentions", required: true)
FormFieldLabel(for: "notify_mentions") { "Direct messages and mentions" }
end
div(class: "flex items-center gap-3") do
RadioButton(id: "notify_nothing", name: "notify", value: "nothing", required: true)
FormFieldLabel(for: "notify_nothing") { "Nothing" }
end
end
FormFieldError()
end
end
Button(type: "submit", class: "w-fit") { "Save" }
end
RUBY
end
render Components::ComponentSetup::Tabs.new(component_name: component)
render Docs::ComponentsTable.new(component_files(component))
end
end
end
3
Add RubyUI::RadioGroup to app/components/ruby_ui/radio_button/radio_group.rb
# frozen_string_literal: true
module RubyUI
# Wraps RadioButtons in role="radiogroup", like shadcn's RadioGroup (Base UI
# + vega). Give it a name with aria: {labelledby:} or aria: {label:}.
# Group-level options (name, disabled, required) stay on each RadioButton,
# as native radios take them.
class RadioGroup < Base
def view_template(&)
div(**attrs, &)
end
private
def default_attrs
{
role: "radiogroup",
data: {slot: "radio-group"},
class: "grid w-full gap-3"
}
end
end
end
Components
| Component | Built using | Source |
|---|---|---|
RadioButton | Phlex | |
RadioButtonDocs | Phlex | |
RadioGroup | Phlex |