Buttons
SimplePrimary, secondary, ghost, and icon buttons with hover states
Updated 1 day ago
Tested & Responsive
Live Demo
Button Variants
Button Sizes
Icon Buttons
Button States
Button Groups
Interactive Demo
Click the button below to see interactive feedback!
Clicks: 0
Split Buttons
<style>
@keyframes ripple {
to {
transform: scale(4);
opacity: 0;
}
}
</style>
<div class="space-y-8">
<!-- Button Variants -->
<div class="space-y-6">
<h3 class="text-lg font-semibold text-forest-800 mb-4">Button Variants</h3>
<div class="flex flex-wrap gap-4 justify-center">
<button
class="px-6 py-3 bg-forest-600 hover:bg-forest-700 focus:bg-forest-700 focus:ring-2 focus:ring-forest-400 focus:ring-offset-2 disabled:bg-forest-300 disabled:cursor-not-allowed text-forest-50 font-medium rounded-lg transition-all duration-200"
data-controller="interactive--button"
data-interactive--button-variant-value="primary"
data-interactive--button-size-value="medium"
data-action="click->interactive--button#click"
>
Primary Button
</button>
<button
class="px-6 py-3 border border-forest-300 hover:bg-forest-100 focus:bg-forest-100 focus:ring-2 focus:ring-forest-400 focus:ring-offset-2 disabled:border-forest-200 disabled:text-forest-400 disabled:cursor-not-allowed text-forest-700 font-medium rounded-lg transition-all duration-200"
data-controller="interactive--button"
data-interactive--button-variant-value="secondary"
data-interactive--button-size-value="medium"
data-action="click->interactive--button#click"
>
Secondary Button
</button>
<button
class="px-6 py-3 hover:bg-forest-100 focus:bg-forest-100 focus:ring-2 focus:ring-forest-400 focus:ring-offset-2 disabled:text-forest-400 disabled:cursor-not-allowed text-forest-700 font-medium rounded-lg transition-all duration-200"
data-controller="interactive--button"
data-interactive--button-variant-value="ghost"
data-interactive--button-size-value="medium"
data-action="click->interactive--button#click"
>
Ghost Button
</button>
</div>
</div>
<!-- Button Sizes -->
<div class="space-y-6">
<h3 class="text-lg font-semibold text-forest-800 mb-4">Button Sizes</h3>
<div class="flex flex-wrap gap-4 items-center justify-center">
<button
class="px-3 py-1.5 bg-forest-600 hover:bg-forest-700 focus:bg-forest-700 focus:ring-2 focus:ring-forest-400 focus:ring-offset-2 text-forest-50 text-sm font-medium rounded-md transition-all duration-200"
data-controller="interactive--button"
data-interactive--button-variant-value="primary"
data-interactive--button-size-value="small"
data-action="click->interactive--button#click"
>
Small
</button>
<button
class="px-6 py-3 bg-forest-600 hover:bg-forest-700 focus:bg-forest-700 focus:ring-2 focus:ring-forest-400 focus:ring-offset-2 text-forest-50 font-medium rounded-lg transition-all duration-200"
data-controller="interactive--button"
data-interactive--button-variant-value="primary"
data-interactive--button-size-value="medium"
data-action="click->interactive--button#click"
>
Medium
</button>
<button
class="px-8 py-4 bg-forest-600 hover:bg-forest-700 focus:bg-forest-700 focus:ring-2 focus:ring-forest-400 focus:ring-offset-2 text-forest-50 text-lg font-medium rounded-xl transition-all duration-200"
data-controller="interactive--button"
data-interactive--button-variant-value="primary"
data-interactive--button-size-value="large"
data-action="click->interactive--button#click"
>
Large
</button>
</div>
</div>
<!-- Icon Buttons -->
<div class="space-y-6">
<h3 class="text-lg font-semibold text-forest-800 mb-4">Icon Buttons</h3>
<div class="flex flex-wrap gap-4 items-center justify-center">
<button
class="flex items-center justify-center p-2 bg-forest-600 hover:bg-forest-700 focus:bg-forest-700 focus:ring-2 focus:ring-forest-400 focus:ring-offset-2 text-forest-50 rounded-lg transition-all duration-200"
data-controller="interactive--button"
data-interactive--button-variant-value="icon"
data-interactive--button-size-value="small"
data-action="click->interactive--button#click"
title="Settings"
>
<svg class="w-4 h-4" data-interactive--button-target="icon" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<div class="hidden animate-spin" data-interactive--button-target="spinner">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
</button>
<button
class="flex items-center justify-center space-x-2 px-4 py-2 bg-forest-600 hover:bg-forest-700 focus:bg-forest-700 focus:ring-2 focus:ring-forest-400 focus:ring-offset-2 text-forest-50 font-medium rounded-lg transition-all duration-200 min-w-[120px]"
data-controller="interactive--button"
data-interactive--button-variant-value="primary"
data-interactive--button-size-value="medium"
data-action="click->interactive--button#click"
>
<svg class="w-5 h-5" data-interactive--button-target="icon" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
<div class="hidden animate-spin" data-interactive--button-target="spinner">
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
<span data-interactive--button-target="text">Add Item</span>
</button>
</div>
</div>
<!-- Button States -->
<div class="space-y-6">
<h3 class="text-lg font-semibold text-forest-800 mb-4">Button States</h3>
<div class="flex flex-wrap gap-4 justify-center">
<button
class="flex items-center justify-center px-6 py-3 bg-forest-600 hover:bg-forest-700 focus:bg-forest-700 focus:ring-2 focus:ring-forest-400 focus:ring-offset-2 text-forest-50 font-medium rounded-lg transition-all duration-200 min-w-[200px]"
data-controller="interactive--button"
data-interactive--button-variant-value="primary"
data-interactive--button-size-value="medium"
data-action="click->interactive--button#simulateAction"
>
<div class="hidden animate-spin mr-2" data-interactive--button-target="spinner">
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
<span data-interactive--button-target="text">Click for Loading State</span>
</button>
<button
class="px-6 py-3 bg-forest-300 text-forest-500 font-medium rounded-lg cursor-not-allowed"
disabled
data-controller="interactive--button"
data-interactive--button-variant-value="primary"
data-interactive--button-size-value="medium"
data-interactive--button-disabled-value="true"
>
Disabled Button
</button>
</div>
</div>
<!-- Button Groups -->
<div class="space-y-6">
<h3 class="text-lg font-semibold text-forest-800 mb-4">Button Groups</h3>
<div class="space-y-4 flex flex-col items-center">
<!-- Basic Button Group -->
<div class="inline-flex rounded-lg border border-forest-300 overflow-hidden">
<button
class="px-4 py-2 bg-forest-600 text-forest-50 hover:bg-forest-700 focus:bg-forest-700 focus:z-10 focus:ring-2 focus:ring-forest-400 font-medium transition-all duration-200"
data-controller="interactive--button"
data-interactive--button-variant-value="primary"
data-interactive--button-size-value="small"
data-action="click->interactive--button#click"
>
Left
</button>
<button
class="px-4 py-2 bg-white border-l border-forest-300 text-forest-700 hover:bg-forest-100 focus:bg-forest-100 focus:z-10 focus:ring-2 focus:ring-forest-400 font-medium transition-all duration-200"
data-controller="interactive--button"
data-interactive--button-variant-value="secondary"
data-interactive--button-size-value="small"
data-action="click->interactive--button#click"
>
Center
</button>
<button
class="px-4 py-2 bg-white border-l border-forest-300 text-forest-700 hover:bg-forest-100 focus:bg-forest-100 focus:z-10 focus:ring-2 focus:ring-forest-400 font-medium transition-all duration-200"
data-controller="interactive--button"
data-interactive--button-variant-value="secondary"
data-interactive--button-size-value="small"
data-action="click->interactive--button#click"
>
Right
</button>
</div>
<!-- Toolbar Group -->
<div class="inline-flex items-center space-x-1 p-1 bg-forest-100 rounded-lg">
<button
class="p-2 text-forest-600 hover:bg-forest-200 focus:bg-forest-200 focus:ring-2 focus:ring-forest-400 rounded-md transition-all duration-200"
data-controller="interactive--button"
data-interactive--button-variant-value="ghost"
data-interactive--button-size-value="small"
data-action="click->interactive--button#click"
title="Bold"
>
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 6.75h9M8.25 12h9m-9 5.25h9" />
</svg>
</button>
<button
class="p-2 text-forest-600 hover:bg-forest-200 focus:bg-forest-200 focus:ring-2 focus:ring-forest-400 rounded-md transition-all duration-200"
data-controller="interactive--button"
data-interactive--button-variant-value="ghost"
data-interactive--button-size-value="small"
data-action="click->interactive--button#click"
title="Italic"
>
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 6h9.75M10.5 6a5.5 5.5 0 1 1-5.093 7.5M10.5 6a5.5 5.5 0 1 0-5.093 7.5m0 0L10.5 18h9.75" />
</svg>
</button>
<button
class="p-2 text-forest-600 hover:bg-forest-200 focus:bg-forest-200 focus:ring-2 focus:ring-forest-400 rounded-md transition-all duration-200"
data-controller="interactive--button"
data-interactive--button-variant-value="ghost"
data-interactive--button-size-value="small"
data-action="click->interactive--button#click"
title="Underline"
>
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3-4.5 16.5" />
</svg>
</button>
<div class="h-6 w-px bg-forest-300 mx-1"></div>
<button
class="p-2 text-forest-600 hover:bg-forest-200 focus:bg-forest-200 focus:ring-2 focus:ring-forest-400 rounded-md transition-all duration-200"
data-controller="interactive--button"
data-interactive--button-variant-value="ghost"
data-interactive--button-size-value="small"
data-action="click->interactive--button#click"
title="Link"
>
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244" />
</svg>
</button>
</div>
</div>
</div>
<!-- Interactive Demo -->
<div class="space-y-6">
<h3 class="text-lg font-semibold text-forest-800 mb-4">Interactive Demo</h3>
<div class="flex flex-col items-center space-y-4" data-controller="counter">
<div class="bg-forest-50 border border-forest-200 rounded-lg p-6 max-w-md w-full text-center">
<p class="text-forest-700 mb-4">Click the button below to see interactive feedback!</p>
<div data-counter-target="display" class="text-2xl font-bold text-forest-800 mb-4">Clicks: 0</div>
<div class="flex gap-2 justify-center">
<button
class="flex items-center justify-center px-6 py-3 bg-forest-600 hover:bg-forest-700 focus:bg-forest-700 focus:ring-2 focus:ring-forest-400 focus:ring-offset-2 text-forest-50 font-medium rounded-lg transition-all duration-200 min-w-[120px]"
data-controller="interactive--button"
data-interactive--button-variant-value="primary"
data-interactive--button-size-value="medium"
data-action="click->interactive--button#click interactive--button:clicked->counter#increment"
>
<span data-interactive--button-target="text">Click Me!</span>
</button>
<button
class="px-4 py-3 border border-forest-300 hover:bg-forest-100 focus:bg-forest-100 focus:ring-2 focus:ring-forest-400 focus:ring-offset-2 text-forest-700 font-medium rounded-lg transition-all duration-200"
data-action="click->counter#reset"
>
Reset
</button>
</div>
</div>
</div>
</div>
<!-- Split Buttons -->
<div class="space-y-6">
<h3 class="text-lg font-semibold text-forest-800 mb-4">Split Buttons</h3>
<div class="flex flex-wrap gap-4 justify-center">
<!-- Split Button with Dropdown -->
<div class="relative inline-flex" data-controller="dropdown" data-dropdown-placement-value="bottom-start">
<button
class="px-6 py-3 bg-forest-600 hover:bg-forest-700 focus:bg-forest-700 focus:ring-2 focus:ring-forest-400 focus:ring-offset-2 text-forest-50 font-medium rounded-l-lg border-r border-forest-500 transition-all duration-200"
data-controller="interactive--button"
data-interactive--button-variant-value="primary"
data-interactive--button-size-value="medium"
data-action="click->interactive--button#click"
>
Save Document
</button>
<button
class="px-3 py-3 bg-forest-600 hover:bg-forest-700 focus:bg-forest-700 focus:ring-2 focus:ring-forest-400 focus:ring-offset-2 text-forest-50 rounded-r-lg transition-all duration-200"
data-dropdown-target="trigger"
data-action="click->dropdown#toggle"
aria-haspopup="true"
aria-expanded="false"
>
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>
</button>
<!-- Dropdown Menu -->
<div
class="hidden absolute z-50 mt-1 w-56 bg-white border border-forest-200 rounded-lg shadow-lg"
data-dropdown-target="menu"
data-action="keydown->dropdown#keydown"
role="menu"
aria-orientation="vertical"
>
<div class="py-1">
<button
class="flex w-full items-center px-4 py-2 text-sm text-forest-700 hover:bg-forest-100 focus:bg-forest-100 focus:outline-none"
role="menuitem"
data-action="click->dropdown#selectItem"
data-value="save"
>
<svg class="w-4 h-4 mr-3" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-2.25m0 0V6.375c0-1.036-.84-1.875-1.875-1.875H3.375A1.875 1.875 0 0 0 1.5 8.25v8.625c0 1.035.84 1.875 1.875 1.875h2.25m0 0h9.375c1.035 0 1.875-.84 1.875-1.875V10.5A1.875 1.875 0 0 0 16.875 8.625H14.25" />
</svg>
Save
</button>
<button
class="flex w-full items-center px-4 py-2 text-sm text-forest-700 hover:bg-forest-100 focus:bg-forest-100 focus:outline-none"
role="menuitem"
data-action="click->dropdown#selectItem"
data-value="save-as"
>
<svg class="w-4 h-4 mr-3" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.666 3.888A2.25 2.25 0 0 0 13.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 0 1-.75.75H9a.75.75 0 0 1-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 0 1-2.25 2.25H6.75A2.25 2.25 0 0 1 4.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 0 1 1.927-.184" />
</svg>
Save As...
</button>
<hr class="my-1 border-forest-200">
<button
class="flex w-full items-center px-4 py-2 text-sm text-forest-700 hover:bg-forest-100 focus:bg-forest-100 focus:outline-none"
role="menuitem"
data-action="click->dropdown#selectItem"
data-value="export-pdf"
>
<svg class="w-4 h-4 mr-3" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
Export as PDF
</button>
<button
class="flex w-full items-center px-4 py-2 text-sm text-forest-700 hover:bg-forest-100 focus:bg-forest-100 focus:outline-none"
role="menuitem"
data-action="click->dropdown#selectItem"
data-value="export-docx"
>
<svg class="w-4 h-4 mr-3" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-2.25m0 0V6.375c0-1.036-.84-1.875-1.875-1.875H3.375A1.875 1.875 0 0 0 1.5 8.25v8.625c0 1.035.84 1.875 1.875 1.875h2.25m0 0h9.375c1.035 0 1.875-.84 1.875-1.875V10.5A1.875 1.875 0 0 0 16.875 8.625H14.25" />
</svg>
Export as Word
</button>
</div>
</div>
</div>
</div>
</div>
</div>
// app/javascript/controllers/button_controller.js
import { Controller } from "@hotwired/stimulus"
// Connects to data-controller="button"
export default class extends Controller {
static targets = ["icon", "text", "spinner"]
static classes = ["loading", "disabled"]
static values = {
loading: Boolean,
disabled: Boolean,
variant: String,
size: String,
clickCount: { type: Number, default: 0 }
}
connect() {
this.updateButtonState()
}
// Toggle loading state
toggleLoading() {
this.loadingValue = !this.loadingValue
}
// Set loading state
setLoading(loading = true) {
this.loadingValue = loading
}
// Toggle disabled state
toggleDisabled() {
this.disabledValue = !this.disabledValue
}
// Set disabled state
setDisabled(disabled = true) {
this.disabledValue = disabled
}
// Handle click events
click(event) {
if (this.disabledValue || this.loadingValue) {
event.preventDefault()
event.stopPropagation()
return false
}
// Increment click counter
this.clickCountValue = this.clickCountValue + 1
// Add visual feedback with ripple effect
this.addRippleEffect(event)
// Dispatch custom event for external handling
this.dispatch("clicked", {
detail: {
variant: this.variantValue,
size: this.sizeValue,
clickCount: this.clickCountValue,
element: this.element
}
})
}
// Add ripple effect for visual feedback
addRippleEffect(event) {
const button = this.element
const rect = button.getBoundingClientRect()
const size = Math.max(rect.width, rect.height)
const x = event.clientX - rect.left - size / 2
const y = event.clientY - rect.top - size / 2
const ripple = document.createElement('span')
ripple.classList.add('ripple')
ripple.style.cssText = `
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.6);
transform: scale(0);
animation: ripple 0.6s linear;
width: ${size}px;
height: ${size}px;
left: ${x}px;
top: ${y}px;
pointer-events: none;
`
button.style.position = 'relative'
button.style.overflow = 'hidden'
button.appendChild(ripple)
setTimeout(() => {
ripple.remove()
}, 600)
}
// Handle loading state changes
loadingValueChanged() {
this.updateButtonState()
}
// Handle disabled state changes
disabledValueChanged() {
this.updateButtonState()
}
// Update button appearance and behavior
updateButtonState() {
const button = this.element
// Update disabled attribute
if (this.disabledValue || this.loadingValue) {
button.disabled = true
button.setAttribute('aria-disabled', 'true')
} else {
button.disabled = false
button.removeAttribute('aria-disabled')
}
// Update loading state
if (this.loadingValue) {
button.classList.add(...this.loadingClasses)
this.showSpinner()
this.hideIcon()
} else {
button.classList.remove(...this.loadingClasses)
this.hideSpinner()
this.showIcon()
}
// Update disabled state (visual only, since loading also disables)
if (this.disabledValue && !this.loadingValue) {
button.classList.add(...this.disabledClasses)
} else {
button.classList.remove(...this.disabledClasses)
}
// Update cursor style
button.style.cursor = (this.disabledValue || this.loadingValue) ? 'not-allowed' : 'pointer'
}
// Show loading spinner
showSpinner() {
this.spinnerTargets.forEach(spinner => {
spinner.classList.remove('hidden')
spinner.classList.add('inline-flex')
spinner.setAttribute('aria-hidden', 'false')
})
}
// Hide loading spinner
hideSpinner() {
this.spinnerTargets.forEach(spinner => {
spinner.classList.add('hidden')
spinner.classList.remove('inline-flex')
spinner.setAttribute('aria-hidden', 'true')
})
}
// Show icon
showIcon() {
this.iconTargets.forEach(icon => {
icon.classList.remove('hidden')
icon.setAttribute('aria-hidden', 'false')
})
}
// Hide icon
hideIcon() {
this.iconTargets.forEach(icon => {
icon.classList.add('hidden')
icon.setAttribute('aria-hidden', 'true')
})
}
// Simulate async action (for demo purposes)
async simulateAction() {
this.setLoading(true)
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 2000))
// Dispatch success event
this.dispatch("action-completed", {
detail: { success: true }
})
} catch (error) {
// Dispatch error event
this.dispatch("action-failed", {
detail: { error: error.message }
})
} finally {
this.setLoading(false)
}
}
}
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.