Checkboxes & Radios
SimpleCustom styled checkboxes and radio buttons with group handling
Updated about 2 months ago
Tested & Responsive
Live Demo
Preview:
Desktop view
Basic Checkbox Group
Checkbox Group with Controls
Radio Button Group
Resize to see how the component adapts to different screen sizes
<div class="space-y-8">
<!-- Basic Checkbox Group -->
<div class="space-y-6">
<h3 class="text-lg font-semibold text-green-800 mb-4 text-center">Basic Checkbox Group</h3>
<div class="max-w-md mx-auto">
<div data-controller="checkbox">
<fieldset>
<legend class="text-sm font-medium text-green-900 mb-4">Select your interests</legend>
<div class="space-y-3">
<div class="flex items-center">
<input
data-checkbox-target="input"
data-action="change->checkbox#toggle"
id="interest-tech"
name="interests[]"
value="technology"
type="checkbox"
class="h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300 rounded"
>
<label for="interest-tech" class="ml-3 text-sm text-gray-700">Technology</label>
</div>
<div class="flex items-center">
<input
data-checkbox-target="input"
data-action="change->checkbox#toggle"
id="interest-design"
name="interests[]"
value="design"
type="checkbox"
class="h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300 rounded"
>
<label for="interest-design" class="ml-3 text-sm text-gray-700">Design</label>
</div>
<div class="flex items-center">
<input
data-checkbox-target="input"
data-action="change->checkbox#toggle"
id="interest-business"
name="interests[]"
value="business"
type="checkbox"
class="h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300 rounded"
>
<label for="interest-business" class="ml-3 text-sm text-gray-700">Business</label>
</div>
</div>
</fieldset>
</div>
</div>
</div>
<!-- Checkbox Group with Controls -->
<div class="space-y-6">
<h3 class="text-lg font-semibold text-green-800 mb-4 text-center">Checkbox Group with Controls</h3>
<div class="max-w-md mx-auto">
<div data-controller="checkbox" data-checkbox-max-selections-value="3">
<div class="flex justify-between items-center mb-4">
<legend class="text-sm font-medium text-green-900">Programming languages (max 3)</legend>
<div class="flex space-x-2">
<button
type="button"
data-action="click->checkbox#selectAll"
class="text-xs text-green-600 hover:text-green-800 underline"
>
Select All
</button>
<button
type="button"
data-action="click->checkbox#selectNone"
class="text-xs text-gray-600 hover:text-gray-800 underline"
>
Select None
</button>
</div>
</div>
<div data-checkbox-target="group" class="space-y-3">
<div class="flex items-center">
<input
data-checkbox-target="input"
data-action="change->checkbox#toggle"
id="lang-javascript"
name="languages[]"
value="javascript"
type="checkbox"
class="h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300 rounded"
>
<label for="lang-javascript" class="ml-3 text-sm text-gray-700">JavaScript</label>
</div>
<div class="flex items-center">
<input
data-checkbox-target="input"
data-action="change->checkbox#toggle"
id="lang-python"
name="languages[]"
value="python"
type="checkbox"
class="h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300 rounded"
>
<label for="lang-python" class="ml-3 text-sm text-gray-700">Python</label>
</div>
<div class="flex items-center">
<input
data-checkbox-target="input"
data-action="change->checkbox#toggle"
id="lang-ruby"
name="languages[]"
value="ruby"
type="checkbox"
class="h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300 rounded"
>
<label for="lang-ruby" class="ml-3 text-sm text-gray-700">Ruby</label>
</div>
</div>
<p data-checkbox-target="error" class="mt-2 text-sm text-red-600 hidden"></p>
</div>
</div>
</div>
<!-- Radio Button Group -->
<div class="space-y-6">
<h3 class="text-lg font-semibold text-green-800 mb-4 text-center">Radio Button Group</h3>
<div class="max-w-md mx-auto">
<div data-controller="radio" data-radio-required-value="true">
<fieldset>
<legend class="text-sm font-medium text-green-900 mb-4">Choose your plan</legend>
<div class="space-y-3">
<div class="flex items-center">
<input
data-radio-target="input"
data-action="change->radio#select"
id="plan-free"
name="plan"
value="free"
type="radio"
class="h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300"
>
<label for="plan-free" class="ml-3 text-sm text-gray-700">Free Plan</label>
</div>
<div class="flex items-center">
<input
data-radio-target="input"
data-action="change->radio#select"
id="plan-pro"
name="plan"
value="pro"
type="radio"
class="h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300"
>
<label for="plan-pro" class="ml-3 text-sm text-gray-700">Pro Plan</label>
</div>
<div class="flex items-center">
<input
data-radio-target="input"
data-action="change->radio#select"
id="plan-enterprise"
name="plan"
value="enterprise"
type="radio"
class="h-4 w-4 text-green-600 focus:ring-green-500 border-gray-300"
>
<label for="plan-enterprise" class="ml-3 text-sm text-gray-700">Enterprise Plan</label>
</div>
</div>
<p data-radio-target="error" class="mt-2 text-sm text-red-600 hidden"></p>
</fieldset>
</div>
</div>
</div>
</div>
import { Controller } from "@hotwired/stimulus"
// Connects to data-controller="checkbox"
export default class extends Controller {
static targets = ["input", "group"]
static values = {
maxSelections: Number,
minSelections: Number,
required: Boolean
}
connect() {
this.updateGroupState()
}
toggle(event) {
const checkbox = event.currentTarget
const isChecked = checkbox.checked
// Handle max selections limit
if (isChecked && this.maxSelectionsValue) {
const checkedCount = this.getCheckedCount()
if (checkedCount > this.maxSelectionsValue) {
checkbox.checked = false
this.showMaxSelectionError()
return
}
}
this.updateGroupState()
this.dispatchChangeEvent(checkbox)
}
selectAll() {
this.inputTargets.forEach(input => {
if (!input.checked && !input.disabled) {
input.checked = true
}
})
this.updateGroupState()
this.dispatchChangeEvent()
}
selectNone() {
this.inputTargets.forEach(input => {
if (input.checked && !input.disabled) {
input.checked = false
}
})
this.updateGroupState()
this.dispatchChangeEvent()
}
getCheckedCount() {
return this.inputTargets.filter(input => input.checked).length
}
getCheckedValues() {
return this.inputTargets
.filter(input => input.checked)
.map(input => input.value)
}
updateGroupState() {
const checkedCount = this.getCheckedCount()
const totalCount = this.inputTargets.length
// Update group data attributes for styling
if (this.hasGroupTarget) {
this.groupTarget.dataset.checkedCount = checkedCount
this.groupTarget.dataset.allChecked = checkedCount === totalCount
this.groupTarget.dataset.noneChecked = checkedCount === 0
this.groupTarget.dataset.someChecked = checkedCount > 0 && checkedCount < totalCount
}
// Update select all/none buttons if they exist
this.updateSelectAllButton(checkedCount, totalCount)
}
updateSelectAllButton(checkedCount, totalCount) {
const selectAllBtn = this.element.querySelector('[data-action*="selectAll"]')
const selectNoneBtn = this.element.querySelector('[data-action*="selectNone"]')
if (selectAllBtn) {
selectAllBtn.disabled = checkedCount === totalCount
}
if (selectNoneBtn) {
selectNoneBtn.disabled = checkedCount === 0
}
}
showMaxSelectionError() {
const errorElement = this.element.querySelector('[data-checkbox-target="error"]')
if (errorElement) {
errorElement.textContent = `You can select a maximum of ${this.maxSelectionsValue} options`
errorElement.classList.remove('hidden')
// Hide error after 3 seconds
setTimeout(() => {
errorElement.classList.add('hidden')
}, 3000)
}
}
validate() {
const checkedCount = this.getCheckedCount()
if (this.requiredValue && checkedCount === 0) {
this.showValidationError("Please select at least one option")
return false
}
if (this.minSelectionsValue && checkedCount < this.minSelectionsValue) {
this.showValidationError(`Please select at least ${this.minSelectionsValue} options`)
return false
}
if (this.maxSelectionsValue && checkedCount > this.maxSelectionsValue) {
this.showValidationError(`Please select no more than ${this.maxSelectionsValue} options`)
return false
}
this.clearValidationError()
return true
}
showValidationError(message) {
const errorElement = this.element.querySelector('[data-checkbox-target="error"]')
if (errorElement) {
errorElement.textContent = message
errorElement.classList.remove('hidden')
}
}
clearValidationError() {
const errorElement = this.element.querySelector('[data-checkbox-target="error"]')
if (errorElement) {
errorElement.textContent = ""
errorElement.classList.add('hidden')
}
}
dispatchChangeEvent(changedInput = null) {
const event = new CustomEvent('checkbox:change', {
detail: {
checkedValues: this.getCheckedValues(),
checkedCount: this.getCheckedCount(),
changedInput: changedInput,
allInputs: this.inputTargets
}
})
this.element.dispatchEvent(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.