Switch
A control that allows the user to toggle between checked and not checked.
Usage
Example
A native checkbox with role="switch": Tab focuses it, Space toggles it, and a label with for= names it.
div(class: "flex items-center gap-3") do
Switch(id: "airplane_mode", name: "airplane_mode")
FormFieldLabel(for: "airplane_mode") { "Airplane Mode" }
end
Checked
div(class: "flex items-center gap-3") do
Switch(id: "notifications", name: "notifications", checked: true)
FormFieldLabel(for: "notifications") { "Enable notifications" }
end
Size
size: :sm renders a smaller switch.
div(class: "flex flex-col gap-4") do
div(class: "flex items-center gap-3") do
Switch(id: "size_sm", size: :sm)
FormFieldLabel(for: "size_sm") { "Small" }
end
div(class: "flex items-center gap-3") do
Switch(id: "size_default")
FormFieldLabel(for: "size_default") { "Default" }
end
end
Disabled
div(class: "flex items-center gap-3") do
Switch(id: "switch_disabled", name: "switch_disabled", disabled: true)
FormFieldLabel(for: "switch_disabled") { "Disabled" }
end
Aria disabled
div(class: "flex items-center gap-3") do
Switch(id: "switch_aria_disabled", name: "switch_aria_disabled", aria: {disabled: "true"})
FormFieldLabel(for: "switch_aria_disabled") { "Aria disabled" }
end
Invalid
div(class: "flex items-center gap-3") do
Switch(id: "switch_invalid", name: "terms", aria: {invalid: "true"})
FormFieldLabel(for: "switch_invalid") { "Accept terms and conditions" }
end
Installation
Using RubyUI CLI
Run the install command
rails g ruby_ui:component Switch
Manual installation
1
Add RubyUI::Switch to app/components/ruby_ui/switch/switch.rb
# frozen_string_literal: true
module RubyUI
# A switch, styled after shadcn's Switch (Base UI + vega). Renders a native
# <input type="checkbox" role="switch"> instead of Base UI's span[role=switch]
# plus hidden input: the input itself is the track (appearance-none) and its
# ::before is the thumb, so it is focusable, toggles with Space, is named by
# a <label for> and exposes its checked state with no JavaScript. ::after
# widens the hit area like shadcn, and peer-* classes on a following label
# keep working.
#
# size: :default (18.4x32px) or :sm (14x24px), as shadcn's size prop.
# include_hidden: renders a hidden input with the same name and
# unchecked_value before the switch, so the form also sends a value when it
# is off (Rails' check_box pattern). checked_value is the switch's value.
class Switch < Base
SIZES = %i[default sm].freeze
BASE = [
"peer relative inline-flex shrink-0 appearance-none rounded-full border border-transparent p-0 align-middle",
"data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px]",
# High contrast: give the drawing back to the browser (as @tailwindcss/forms does).
"forced-colors:appearance-auto forced-colors:before:hidden",
"bg-input dark:bg-input/80 checked:bg-primary dark:checked:bg-primary checked:bg-none",
"text-inherit shadow-xs transition-all outline-hidden",
"after:absolute after:-inset-x-3 after:-inset-y-2",
"focus:ring-0 focus:ring-offset-0 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50",
"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",
"disabled:cursor-not-allowed disabled:opacity-50",
"aria-disabled:cursor-not-allowed aria-disabled:opacity-50 aria-disabled:pointer-events-none",
# Thumb.
"before:pointer-events-none before:absolute before:inset-y-0 before:left-0 before:my-auto before:block before:rounded-full",
"before:bg-background dark:before:bg-foreground dark:checked:before:bg-primary-foreground",
"before:transition-transform before:translate-x-0 checked:before:translate-x-[calc(100%-2px)]",
"data-[size=default]:before:size-4 data-[size=sm]:before:size-3"
].freeze
def initialize(size: :default, include_hidden: true, checked_value: "1", unchecked_value: "0", **attrs)
@size = size.to_sym
raise ArgumentError, "size: must be one of #{SIZES.join(", ")}" unless SIZES.include?(@size)
@include_hidden = include_hidden
@checked_value = checked_value
@unchecked_value = unchecked_value
super(**attrs)
end
def view_template
if @include_hidden
input(type: "hidden", name: attrs[:name], value: @unchecked_value, disabled: attrs[:disabled], form: attrs[:form], autocomplete: "off")
end
input(**attrs, type: "checkbox", role: "switch", value: @checked_value)
end
private
def default_attrs
{
type: "checkbox",
role: "switch",
data: {slot: "switch", size: @size},
class: BASE
}
end
end
end
2
Add RubyUI::SwitchDocs to app/components/ruby_ui/switch/switch_docs.rb
# frozen_string_literal: true
class Views::Docs::Switch < Views::Base
def view_template
component = "Switch"
div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do
render Docs::Header.new(title: "Switch", description: "A control that allows the user to toggle between checked and not checked.")
Heading(level: 2) { "Usage" }
render Docs::VisualCodeExample.new(title: "Example", description: "A native checkbox with role=\"switch\": Tab focuses it, Space toggles it, and a label with for= names it.", context: self) do
<<~RUBY
div(class: "flex items-center gap-3") do
Switch(id: "airplane_mode", name: "airplane_mode")
FormFieldLabel(for: "airplane_mode") { "Airplane Mode" }
end
RUBY
end
render Docs::VisualCodeExample.new(title: "Checked", context: self) do
<<~RUBY
div(class: "flex items-center gap-3") do
Switch(id: "notifications", name: "notifications", checked: true)
FormFieldLabel(for: "notifications") { "Enable notifications" }
end
RUBY
end
render Docs::VisualCodeExample.new(title: "Size", description: "size: :sm renders a smaller switch.", context: self) do
<<~RUBY
div(class: "flex flex-col gap-4") do
div(class: "flex items-center gap-3") do
Switch(id: "size_sm", size: :sm)
FormFieldLabel(for: "size_sm") { "Small" }
end
div(class: "flex items-center gap-3") do
Switch(id: "size_default")
FormFieldLabel(for: "size_default") { "Default" }
end
end
RUBY
end
render Docs::VisualCodeExample.new(title: "Disabled", context: self) do
<<~RUBY
div(class: "flex items-center gap-3") do
Switch(id: "switch_disabled", name: "switch_disabled", disabled: true)
FormFieldLabel(for: "switch_disabled") { "Disabled" }
end
RUBY
end
render Docs::VisualCodeExample.new(title: "Aria Disabled", context: self) do
<<~RUBY
div(class: "flex items-center gap-3") do
Switch(id: "switch_aria_disabled", name: "switch_aria_disabled", aria: {disabled: "true"})
FormFieldLabel(for: "switch_aria_disabled") { "Aria disabled" }
end
RUBY
end
render Docs::VisualCodeExample.new(title: "Invalid", context: self) do
<<~RUBY
div(class: "flex items-center gap-3") do
Switch(id: "switch_invalid", name: "terms", aria: {invalid: "true"})
FormFieldLabel(for: "switch_invalid") { "Accept terms and conditions" }
end
RUBY
end
render Docs::VisualCodeExample.new(title: "With flag include_hidden false", description: "By default a hidden input with the same name sends unchecked_value (\"0\") when the switch is off, like Rails' check_box. include_hidden: false skips it.", context: self) do
<<~RUBY
div(class: "flex items-center gap-3") do
Switch(id: "switch_no_hidden", name: "switch", include_hidden: false)
FormFieldLabel(for: "switch_no_hidden") { "Without hidden input" }
end
RUBY
end
render Components::ComponentSetup::Tabs.new(component_name: component)
render Docs::ComponentsTable.new(component_files(component))
end
end
end
Components
| Component | Built using | Source |
|---|---|---|
Switch | Phlex | |
SwitchDocs | Phlex |