Toggle Switches
SimpleOn/off toggle switches with smooth animations and accessibility features
Updated about 2 months ago
Tested & Responsive
Live Demo
Preview:
Desktop view
Basic Toggle
Toggle with Description
Receive emails about new products and features.
Toggle Sizes
Resize to see how the component adapts to different screen sizes
<div class="space-y-8">
<!-- Basic Toggle -->
<div class="space-y-6">
<h3 class="text-lg font-semibold text-green-800 mb-4 text-center">Basic Toggle</h3>
<div class="max-w-md mx-auto">
<div data-controller="toggle" class="flex items-center">
<input
data-toggle-target="input"
type="checkbox"
id="notifications"
name="notifications"
value="enabled"
class="sr-only"
>
<button
data-toggle-target="switch"
data-action="click->toggle#toggle keydown->toggle#handleKeydown"
type="button"
class="relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent bg-gray-200 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2"
role="switch"
aria-checked="false"
>
<span class="sr-only">Enable notifications</span>
<span
data-toggle-target="button"
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out translate-x-0"
></span>
</button>
<label data-toggle-target="label" for="notifications" class="ml-3 text-sm font-medium text-gray-700">
Enable notifications
</label>
</div>
</div>
</div>
<!-- Toggle with Description -->
<div class="space-y-6">
<h3 class="text-lg font-semibold text-green-800 mb-4 text-center">Toggle with Description</h3>
<div class="max-w-md mx-auto">
<div data-controller="toggle" class="flex items-start">
<div class="flex items-center h-5">
<input
data-toggle-target="input"
type="checkbox"
id="marketing-emails"
name="marketing_emails"
value="enabled"
class="sr-only"
>
<button
data-toggle-target="switch"
data-action="click->toggle#toggle keydown->toggle#handleKeydown"
type="button"
class="relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent bg-gray-200 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2"
role="switch"
aria-checked="false"
>
<span class="sr-only">Enable marketing emails</span>
<span
data-toggle-target="button"
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out translate-x-0"
></span>
</button>
</div>
<div class="ml-3">
<label data-toggle-target="label" for="marketing-emails" class="text-sm font-medium text-gray-700">
Marketing emails
</label>
<p class="text-sm text-gray-500">Receive emails about new products and features.</p>
</div>
</div>
</div>
</div>
<!-- Toggle Sizes -->
<div class="space-y-6">
<h3 class="text-lg font-semibold text-green-800 mb-4 text-center">Toggle Sizes</h3>
<div class="max-w-md mx-auto">
<div class="space-y-4">
<!-- Small -->
<div data-controller="toggle" data-toggle-size-value="small" class="flex items-center">
<input
data-toggle-target="input"
type="checkbox"
id="small-setting"
name="small_setting"
class="sr-only"
>
<button
data-toggle-target="switch"
data-action="click->toggle#toggle keydown->toggle#handleKeydown"
type="button"
class="relative inline-flex h-4 w-7 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent bg-gray-200 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2"
role="switch"
aria-checked="false"
>
<span class="sr-only">Small toggle</span>
<span
data-toggle-target="button"
class="pointer-events-none inline-block h-3 w-3 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out translate-x-0"
></span>
</button>
<label data-toggle-target="label" for="small-setting" class="ml-3 text-sm text-gray-700">
Small toggle
</label>
</div>
<!-- Medium (Default) -->
<div data-controller="toggle" class="flex items-center">
<input
data-toggle-target="input"
type="checkbox"
id="medium-setting"
name="medium_setting"
class="sr-only"
>
<button
data-toggle-target="switch"
data-action="click->toggle#toggle keydown->toggle#handleKeydown"
type="button"
class="relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent bg-gray-200 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2"
role="switch"
aria-checked="false"
>
<span class="sr-only">Medium toggle</span>
<span
data-toggle-target="button"
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out translate-x-0"
></span>
</button>
<label data-toggle-target="label" for="medium-setting" class="ml-3 text-sm text-gray-700">
Medium toggle (default)
</label>
</div>
<!-- Large -->
<div data-controller="toggle" data-toggle-size-value="large" class="flex items-center">
<input
data-toggle-target="input"
type="checkbox"
id="large-setting"
name="large_setting"
class="sr-only"
>
<button
data-toggle-target="switch"
data-action="click->toggle#toggle keydown->toggle#handleKeydown"
type="button"
class="relative inline-flex h-8 w-14 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent bg-gray-200 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2"
role="switch"
aria-checked="false"
>
<span class="sr-only">Large toggle</span>
<span
data-toggle-target="button"
class="pointer-events-none inline-block h-7 w-7 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out translate-x-0"
></span>
</button>
<label data-toggle-target="label" for="large-setting" class="ml-3 text-sm text-gray-700">
Large toggle
</label>
</div>
</div>
</div>
</div>
</div>
import { Controller } from "@hotwired/stimulus"
// Connects to data-controller="toggle"
export default class extends Controller {
static targets = ["input", "switch", "label", "button"]
static values = {
disabled: Boolean,
size: String // small, medium, large
}
connect() {
this.updateVisualState()
}
toggle(event) {
if (this.disabledValue) {
event.preventDefault()
return
}
// Toggle the input state
this.inputTarget.checked = !this.inputTarget.checked
// Update visual state
this.updateVisualState()
// Dispatch change event
this.dispatchChangeEvent()
}
updateVisualState() {
const isChecked = this.inputTarget.checked
const isDisabled = this.disabledValue || this.inputTarget.disabled
// Update switch visual state
if (this.hasSwitchTarget) {
if (isChecked) {
this.switchTarget.classList.add('bg-green-600')
this.switchTarget.classList.remove('bg-gray-200')
} else {
this.switchTarget.classList.remove('bg-green-600')
this.switchTarget.classList.add('bg-gray-200')
}
// Update disabled state
if (isDisabled) {
this.switchTarget.classList.add('opacity-50', 'cursor-not-allowed')
this.switchTarget.classList.remove('cursor-pointer')
} else {
this.switchTarget.classList.remove('opacity-50', 'cursor-not-allowed')
this.switchTarget.classList.add('cursor-pointer')
}
// Update the toggle button position
if (this.hasButtonTarget) {
// Remove all possible translate classes
this.buttonTarget.classList.remove('translate-x-0', 'translate-x-3', 'translate-x-5', 'translate-x-6')
if (isChecked) {
// Set translate based on size
switch (this.sizeValue) {
case 'small':
this.buttonTarget.classList.add('translate-x-3')
break
case 'large':
this.buttonTarget.classList.add('translate-x-6')
break
default: // medium
this.buttonTarget.classList.add('translate-x-5')
break
}
} else {
this.buttonTarget.classList.add('translate-x-0')
}
}
}
// Update label state if present
if (this.hasLabelTarget) {
if (isDisabled) {
this.labelTarget.classList.add('text-gray-400')
this.labelTarget.classList.remove('text-gray-700')
} else {
this.labelTarget.classList.remove('text-gray-400')
this.labelTarget.classList.add('text-gray-700')
}
}
// Update data attributes for CSS styling
this.element.dataset.checked = isChecked
this.element.dataset.disabled = isDisabled
}
enable() {
this.disabledValue = false
this.inputTarget.disabled = false
this.updateVisualState()
}
disable() {
this.disabledValue = true
this.inputTarget.disabled = true
this.updateVisualState()
}
setValue(value) {
this.inputTarget.checked = Boolean(value)
this.updateVisualState()
this.dispatchChangeEvent()
}
getValue() {
return this.inputTarget.checked
}
dispatchChangeEvent() {
// Dispatch native change event on the input
const changeEvent = new Event('change', { bubbles: true })
this.inputTarget.dispatchEvent(changeEvent)
// Dispatch custom toggle event
const toggleEvent = new CustomEvent('toggle:change', {
detail: {
checked: this.inputTarget.checked,
value: this.inputTarget.value,
input: this.inputTarget
}
})
this.element.dispatchEvent(toggleEvent)
}
// Handle keyboard events
handleKeydown(event) {
if (event.key === ' ' || event.key === 'Enter') {
event.preventDefault()
this.toggle(event)
}
}
}
Installation & Usage
1
Copy the ERB template
Copy the ERB code from the template tab above and paste it into your Rails view file.
2
Add the Stimulus controller
Create the Stimulus controller file in your JavaScript controllers directory.