Buttons
SimplePrimary, secondary, ghost, and icon buttons with hover states and loading animations
Live Demo
Button Variants
Primary, secondary, and ghost button styles
Resize to see how the component adapts to different screen sizes
📖 Button Variants Overview
This example demonstrates three essential button variants: primary, secondary, and ghost styles. Each variant serves a different purpose in your interface hierarchy and visual design system.
🎯 Button Hierarchy
- • Primary - Main call-to-action buttons with solid forest background
- • Secondary - Supporting actions with outlined border style
- • Ghost - Subtle actions with transparent background and hover effects
- • Consistent spacing and typography across all variants
- • Hover and focus states for enhanced user feedback
💡 Usage Guidelines
- • Use primary for the most important action on a page
- • Use secondary for important but not primary actions
- • Use ghost for subtle or tertiary actions
- • Maintain visual hierarchy with proper variant selection
🏗️ Button Variants HTML Structure
Each button variant uses the same semantic structure with different CSS classes to achieve distinct visual appearances.
📋 Stimulus Integration
data-controller="interactive-buttons-variants"
Connects buttons to the variants controller for ripple effects
data-action="click->interactive-buttons-variants#addRipple"
Triggers ripple animation on button clicks
data-interactive-buttons-variants-target="button"
Identifies clickable button elements for interaction
♿ Semantic HTML
- • Uses
<button>
elements for proper semantics - • Includes descriptive button text for screen readers
- • Maintains focus states for keyboard navigation
- • ARIA attributes added automatically by browser for buttons
🎨 Button Variants Styling
Each button variant uses a specific combination of Tailwind classes to create distinct visual styles while maintaining consistency.
🎨 Variant Color Schemes
Primary Button
bg-forest-600
hover:bg-forest-700
Secondary Button
border-forest-600
hover:bg-forest-50
Ghost Button
bg-transparent
hover:bg-forest-100
📐 Shared Layout Classes
px-6 py-3
- Consistent paddingfont-medium
- Medium font weightrounded-lg
- Rounded cornerstransition-all
- Smooth hover effectsduration-200
- 200ms transitions📱 Responsive Considerations
- • Buttons maintain consistent sizing across all screen sizes
- • Touch-friendly 44px minimum height on mobile devices
- • Adequate spacing between buttons on smaller screens
- • Font size remains readable across device types
⚡ Button Variants JavaScript Logic
The Button Variants controller provides interactive ripple effects that work across all three button variants, enhancing user feedback without affecting the core button functionality.
🎯 Controller Features
Targets
button
- All variant buttons for ripple effects
Actions
addRipple
- Creates visual feedback on click
Values
No configuration values needed for basic ripple effects
Lifecycle
Automatic setup with no cleanup required
🔄 Ripple Effect Flow
- 1. User clicks any button variant (primary, secondary, or ghost)
- 2. Click event triggers the
addRipple
method - 3. Controller calculates click position relative to button
- 4. Creates ripple element with appropriate size and position
- 5. Animates ripple expansion with CSS transforms
- 6. Removes ripple element after animation completes
🔥 Interactive Features
- • Universal ripple effect works on all button variants
- • Ripple color automatically adapts to button variant
- • Non-blocking animations that don't interfere with button actions
- • Proper cleanup prevents memory leaks from dynamic elements
<style>
@keyframes ripple {
to {
transform: scale(4);
opacity: 0;
}
}
</style>
<div class="flex flex-wrap gap-4 justify-center">
<button
class="px-6 py-3 bg-green-600 hover:bg-green-700 focus:bg-green-700 focus:ring-2 focus:ring-green-400 focus:ring-offset-2 disabled:bg-green-300 disabled:cursor-not-allowed text-green-50 font-medium rounded-lg transition-all duration-200"
data-controller="buttons-variants"
data-buttons-variants-variant-value="primary"
data-buttons-variants-size-value="medium"
data-action="click->buttons-variants#click"
>
Primary Button
</button>
<button
class="px-6 py-3 border border-green-300 hover:bg-green-100 focus:bg-green-100 focus:ring-2 focus:ring-green-400 focus:ring-offset-2 disabled:border-green-200 disabled:text-green-400 disabled:cursor-not-allowed text-green-700 font-medium rounded-lg transition-all duration-200"
data-controller="buttons-variants"
data-buttons-variants-variant-value="secondary"
data-buttons-variants-size-value="medium"
data-action="click->buttons-variants#click"
>
Secondary Button
</button>
<button
class="px-6 py-3 hover:bg-green-100 focus:bg-green-100 focus:ring-2 focus:ring-green-400 focus:ring-offset-2 disabled:text-green-400 disabled:cursor-not-allowed text-green-700 font-medium rounded-lg transition-all duration-200"
data-controller="buttons-variants"
data-buttons-variants-variant-value="ghost"
data-buttons-variants-size-value="medium"
data-action="click->buttons-variants#click"
>
Ghost Button
</button>
</div>
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static values = {
variant: String,
size: String
}
connect() {
console.log(`${this.variantValue} button connected`)
}
click(event) {
// Add visual feedback with ripple effect
this.addRippleEffect(event)
// Dispatch custom event
this.dispatch("clicked", {
detail: {
variant: this.variantValue,
size: this.sizeValue,
element: this.element
}
})
}
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)
}
}
Button Sizes
Small, medium, and large button sizes
Resize to see how the component adapts to different screen sizes
📖 Button Sizes Overview
Small, medium, and large button sizes
💡 Key Features
- • Button Sizes functionality with Stimulus controller
- • Buttons, modals, dropdowns, and interactive UI components
- • Responsive design optimized for all screen sizes
- • Accessible markup with proper semantic HTML
- • Modern CSS transitions and interactive effects
- • Enhanced with buttons capabilities
- • Enhanced with interactive capabilities
- • Enhanced with click capabilities
- • Enhanced with primary capabilities
- • Enhanced with secondary capabilities
- • Enhanced with ghost capabilities
- • Enhanced with icon capabilities
- • Enhanced with loading capabilities
🏗️ Button Sizes HTML Structure
This button sizes component uses semantic HTML structure with Stimulus data attributes to connect HTML elements to JavaScript functionality.
📋 Stimulus Data Attributes
data-controller="interactive-buttons-sizes"
Connects this HTML element to the Button Sizes Stimulus controller
data-action="click->interactive-buttons-sizes#method"
Defines click events that trigger button sizes controller methods
data-interactive-buttons-sizes-target="element"
Identifies elements that the Button Sizes controller can reference and manipulate
♿ Accessibility Features
- • Semantic HTML elements provide screen reader context
- • ARIA attributes enhance button sizes accessibility
- • Keyboard navigation fully supported
- • Focus management for interactive elements
🎨 Button Sizes Tailwind Classes
This button sizes component uses Tailwind CSS utility classes with the forest theme color palette for styling and responsive design.
🎨 Interactive Elements Colors
bg-amber-600
Primary
bg-amber-100
Light
bg-honey-400
Accent
📐 Layout & Spacing
p-4
- Padding for button sizes contentmb-4
- Margin bottom between elementsspace-y-2
- Vertical spacing in listsrounded-lg
- Rounded corners for modern look📱 Responsive Button Sizes Design
- •
sm:
- Small screens (640px+) optimized for button sizes - •
md:
- Medium screens (768px+) enhanced layout - •
lg:
- Large screens (1024px+) full functionality - • Mobile-first approach ensures button sizes works on all devices
⚡ Button Sizes JavaScript Logic
The Button Sizes component uses a dedicated Stimulus controller (interactive-buttons-sizes
) to handle button sizes interactions and manage component state.
🎯 Button Sizes Controller Features
Targets
DOM elements the button sizes controller can reference and manipulate
Values
Configuration data for button sizes behavior passed from HTML
Actions
Methods triggered by button sizes user events and interactions
Lifecycle
Setup and cleanup methods for button sizes initialization
🔄 Button Sizes Event Flow
- 1. User interacts with button sizes element (click, hover, input, etc.)
- 2. Stimulus detects event through
data-action
attribute - 3. Button Sizes controller method executes with access to targets and values
- 4. Controller updates DOM with new button sizes state or visual changes
- 5. CSS transitions provide smooth visual feedback for button sizes interactions
<style>
@keyframes ripple {
to {
transform: scale(4);
opacity: 0;
}
}
</style>
<div class="flex flex-wrap gap-4 items-center justify-center">
<button
class="px-3 py-1.5 bg-green-600 hover:bg-green-700 focus:bg-green-700 focus:ring-2 focus:ring-green-400 focus:ring-offset-2 text-green-50 text-sm font-medium rounded-md transition-all duration-200"
data-controller="buttons-sizes"
data-buttons-sizes-size-value="small"
data-action="click->buttons-sizes#click"
>
Small
</button>
<button
class="px-6 py-3 bg-green-600 hover:bg-green-700 focus:bg-green-700 focus:ring-2 focus:ring-green-400 focus:ring-offset-2 text-green-50 font-medium rounded-lg transition-all duration-200"
data-controller="buttons-sizes"
data-buttons-sizes-size-value="medium"
data-action="click->buttons-sizes#click"
>
Medium
</button>
<button
class="px-8 py-4 bg-green-600 hover:bg-green-700 focus:bg-green-700 focus:ring-2 focus:ring-green-400 focus:ring-offset-2 text-green-50 text-lg font-medium rounded-xl transition-all duration-200"
data-controller="buttons-sizes"
data-buttons-sizes-size-value="large"
data-action="click->buttons-sizes#click"
>
Large
</button>
</div>
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static values = {
size: String
}
connect() {
console.log(`${this.sizeValue} size button connected`)
}
click(event) {
// Add visual feedback
this.addRippleEffect(event)
// Show size feedback
this.showSizeFeedback()
// Dispatch custom event
this.dispatch("clicked", {
detail: {
size: this.sizeValue,
element: this.element
}
})
}
showSizeFeedback() {
const originalText = this.element.textContent
this.element.textContent = `${this.sizeValue} clicked!`
setTimeout(() => {
this.element.textContent = originalText
}, 1000)
}
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)
}
}
Icon Buttons
Buttons with icons and loading spinners
Resize to see how the component adapts to different screen sizes
📖 Icon Buttons Overview
Buttons with icons and loading spinners
💡 Key Features
- • Icon Buttons functionality with Stimulus controller
- • Buttons, modals, dropdowns, and interactive UI components
- • Responsive design optimized for all screen sizes
- • Accessible markup with proper semantic HTML
- • Modern CSS transitions and interactive effects
- • Enhanced with buttons capabilities
- • Enhanced with interactive capabilities
- • Enhanced with click capabilities
- • Enhanced with primary capabilities
- • Enhanced with secondary capabilities
- • Enhanced with ghost capabilities
- • Enhanced with icon capabilities
- • Enhanced with loading capabilities
🏗️ Icon Buttons HTML Structure
This icon buttons component uses semantic HTML structure with Stimulus data attributes to connect HTML elements to JavaScript functionality.
📋 Stimulus Data Attributes
data-controller="interactive-buttons-icons"
Connects this HTML element to the Icon Buttons Stimulus controller
data-action="click->interactive-buttons-icons#method"
Defines click events that trigger icon buttons controller methods
data-interactive-buttons-icons-target="element"
Identifies elements that the Icon Buttons controller can reference and manipulate
♿ Accessibility Features
- • Semantic HTML elements provide screen reader context
- • ARIA attributes enhance icon buttons accessibility
- • Keyboard navigation fully supported
- • Focus management for interactive elements
🎨 Icon Buttons Tailwind Classes
This icon buttons component uses Tailwind CSS utility classes with the forest theme color palette for styling and responsive design.
🎨 Interactive Elements Colors
bg-amber-600
Primary
bg-amber-100
Light
bg-honey-400
Accent
📐 Layout & Spacing
p-4
- Padding for icon buttons contentmb-4
- Margin bottom between elementsspace-y-2
- Vertical spacing in listsrounded-lg
- Rounded corners for modern look📱 Responsive Icon Buttons Design
- •
sm:
- Small screens (640px+) optimized for icon buttons - •
md:
- Medium screens (768px+) enhanced layout - •
lg:
- Large screens (1024px+) full functionality - • Mobile-first approach ensures icon buttons works on all devices
⚡ Icon Buttons JavaScript Logic
The Icon Buttons component uses a dedicated Stimulus controller (interactive-buttons-icons
) to handle icon buttons interactions and manage component state.
🎯 Icon Buttons Controller Features
Targets
DOM elements the icon buttons controller can reference and manipulate
Values
Configuration data for icon buttons behavior passed from HTML
Actions
Methods triggered by icon buttons user events and interactions
Lifecycle
Setup and cleanup methods for icon buttons initialization
🔄 Icon Buttons Event Flow
- 1. User interacts with icon buttons element (click, hover, input, etc.)
- 2. Stimulus detects event through
data-action
attribute - 3. Icon Buttons controller method executes with access to targets and values
- 4. Controller updates DOM with new icon buttons state or visual changes
- 5. CSS transitions provide smooth visual feedback for icon buttons interactions
<div class="flex flex-wrap gap-4 items-center justify-center">
<button
class="flex items-center justify-center p-2 bg-green-600 hover:bg-green-700 focus:bg-green-700 focus:ring-2 focus:ring-green-400 focus:ring-offset-2 text-green-50 rounded-lg transition-all duration-200"
data-controller="buttons-icons"
data-buttons-icons-variant-value="icon"
data-buttons-icons-size-value="small"
data-action="click->buttons-icons#click"
title="Settings"
>
<svg class="w-4 h-4" data-buttons-icons-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-buttons-icons-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-green-600 hover:bg-green-700 focus:bg-green-700 focus:ring-2 focus:ring-green-400 focus:ring-offset-2 text-green-50 font-medium rounded-lg transition-all duration-200 min-w-[120px]"
data-controller="buttons-icons"
data-buttons-icons-variant-value="primary"
data-buttons-icons-size-value="medium"
data-action="click->buttons-icons#click"
>
<svg class="w-5 h-5" data-buttons-icons-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-buttons-icons-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-buttons-icons-target="text">Add Item</span>
</button>
</div>
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["icon", "text", "spinner"]
static values = {
variant: String,
size: String
}
connect() {
console.log("Icon button connected")
}
click(event) {
// Show loading state briefly for demo
this.showLoadingState()
// Add ripple effect
this.addRippleEffect(event)
// Dispatch custom event
this.dispatch("clicked", {
detail: {
variant: this.variantValue,
element: this.element
}
})
}
showLoadingState() {
// Hide icon, show spinner
this.iconTargets.forEach(icon => {
icon.classList.add('hidden')
})
this.spinnerTargets.forEach(spinner => {
spinner.classList.remove('hidden')
spinner.classList.add('inline-flex')
})
// Update text if present
if (this.hasTextTarget) {
this.textTarget.textContent = "Loading..."
}
// Reset after 1 second
setTimeout(() => {
this.resetState()
}, 1000)
}
resetState() {
// Show icon, hide spinner
this.iconTargets.forEach(icon => {
icon.classList.remove('hidden')
})
this.spinnerTargets.forEach(spinner => {
spinner.classList.add('hidden')
spinner.classList.remove('inline-flex')
})
// Reset text if present
if (this.hasTextTarget) {
this.textTarget.textContent = "Add Item"
}
}
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)
}
}
Button States
Loading and disabled button states
Resize to see how the component adapts to different screen sizes
📖 Button States Overview
Loading and disabled button states
💡 Key Features
- • Button States functionality with Stimulus controller
- • Buttons, modals, dropdowns, and interactive UI components
- • Responsive design optimized for all screen sizes
- • Accessible markup with proper semantic HTML
- • Modern CSS transitions and interactive effects
- • Enhanced with buttons capabilities
- • Enhanced with interactive capabilities
- • Enhanced with click capabilities
- • Enhanced with primary capabilities
- • Enhanced with secondary capabilities
- • Enhanced with ghost capabilities
- • Enhanced with icon capabilities
- • Enhanced with loading capabilities
🏗️ Button States HTML Structure
This button states component uses semantic HTML structure with Stimulus data attributes to connect HTML elements to JavaScript functionality.
📋 Stimulus Data Attributes
data-controller="interactive-buttons-states"
Connects this HTML element to the Button States Stimulus controller
data-action="click->interactive-buttons-states#method"
Defines click events that trigger button states controller methods
data-interactive-buttons-states-target="element"
Identifies elements that the Button States controller can reference and manipulate
♿ Accessibility Features
- • Semantic HTML elements provide screen reader context
- • ARIA attributes enhance button states accessibility
- • Keyboard navigation fully supported
- • Focus management for interactive elements
🎨 Button States Tailwind Classes
This button states component uses Tailwind CSS utility classes with the forest theme color palette for styling and responsive design.
🎨 Interactive Elements Colors
bg-amber-600
Primary
bg-amber-100
Light
bg-honey-400
Accent
📐 Layout & Spacing
p-4
- Padding for button states contentmb-4
- Margin bottom between elementsspace-y-2
- Vertical spacing in listsrounded-lg
- Rounded corners for modern look📱 Responsive Button States Design
- •
sm:
- Small screens (640px+) optimized for button states - •
md:
- Medium screens (768px+) enhanced layout - •
lg:
- Large screens (1024px+) full functionality - • Mobile-first approach ensures button states works on all devices
⚡ Button States JavaScript Logic
The Button States component uses a dedicated Stimulus controller (interactive-buttons-states
) to handle button states interactions and manage component state.
🎯 Button States Controller Features
Targets
DOM elements the button states controller can reference and manipulate
Values
Configuration data for button states behavior passed from HTML
Actions
Methods triggered by button states user events and interactions
Lifecycle
Setup and cleanup methods for button states initialization
🔄 Button States Event Flow
- 1. User interacts with button states element (click, hover, input, etc.)
- 2. Stimulus detects event through
data-action
attribute - 3. Button States controller method executes with access to targets and values
- 4. Controller updates DOM with new button states state or visual changes
- 5. CSS transitions provide smooth visual feedback for button states interactions
<div class="flex flex-wrap gap-4 justify-center">
<button
class="flex items-center justify-center px-6 py-3 bg-green-600 hover:bg-green-700 focus:bg-green-700 focus:ring-2 focus:ring-green-400 focus:ring-offset-2 text-green-50 font-medium rounded-lg transition-all duration-200 min-w-[200px]"
data-controller="buttons-states"
data-action="click->buttons-states#simulateAction"
>
<div class="hidden animate-spin mr-2" data-buttons-states-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-buttons-states-target="text">Click for Loading State</span>
</button>
<button
class="px-6 py-3 bg-green-300 text-green-500 font-medium rounded-lg cursor-not-allowed"
disabled
data-controller="buttons-states"
data-buttons-states-disabled-value="true"
>
Disabled Button
</button>
</div>
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["spinner", "text"]
static values = {
loading: Boolean,
disabled: Boolean
}
connect() {
this.updateButtonState()
}
click(event) {
if (this.disabledValue || this.loadingValue) {
event.preventDefault()
event.stopPropagation()
return false
}
// Add ripple effect
this.addRippleEffect(event)
// Dispatch custom event
this.dispatch("clicked", {
detail: {
element: this.element
}
})
}
async simulateAction() {
this.loadingValue = 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.loadingValue = false
}
}
loadingValueChanged() {
this.updateButtonState()
}
disabledValueChanged() {
this.updateButtonState()
}
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.hasSpinnerTarget) {
if (this.loadingValue) {
this.spinnerTarget.classList.remove('hidden')
this.spinnerTarget.classList.add('inline-flex')
} else {
this.spinnerTarget.classList.add('hidden')
this.spinnerTarget.classList.remove('inline-flex')
}
}
// Update text
if (this.hasTextTarget) {
this.textTarget.textContent = this.loadingValue ? "Loading..." : "Click for Loading State"
}
// Update cursor style
button.style.cursor = (this.disabledValue || this.loadingValue) ? 'not-allowed' : 'pointer'
}
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)
}
}
Button Groups
Connected button groups and toolbars
Resize to see how the component adapts to different screen sizes
📖 Button Groups Overview
Connected button groups and toolbars
💡 Key Features
- • Button Groups functionality with Stimulus controller
- • Buttons, modals, dropdowns, and interactive UI components
- • Responsive design optimized for all screen sizes
- • Accessible markup with proper semantic HTML
- • Modern CSS transitions and interactive effects
- • Enhanced with buttons capabilities
- • Enhanced with interactive capabilities
- • Enhanced with click capabilities
- • Enhanced with primary capabilities
- • Enhanced with secondary capabilities
- • Enhanced with ghost capabilities
- • Enhanced with icon capabilities
- • Enhanced with loading capabilities
🏗️ Button Groups HTML Structure
This button groups component uses semantic HTML structure with Stimulus data attributes to connect HTML elements to JavaScript functionality.
📋 Stimulus Data Attributes
data-controller="interactive-buttons-groups"
Connects this HTML element to the Button Groups Stimulus controller
data-action="click->interactive-buttons-groups#method"
Defines click events that trigger button groups controller methods
data-interactive-buttons-groups-target="element"
Identifies elements that the Button Groups controller can reference and manipulate
♿ Accessibility Features
- • Semantic HTML elements provide screen reader context
- • ARIA attributes enhance button groups accessibility
- • Keyboard navigation fully supported
- • Focus management for interactive elements
🎨 Button Groups Tailwind Classes
This button groups component uses Tailwind CSS utility classes with the forest theme color palette for styling and responsive design.
🎨 Interactive Elements Colors
bg-amber-600
Primary
bg-amber-100
Light
bg-honey-400
Accent
📐 Layout & Spacing
p-4
- Padding for button groups contentmb-4
- Margin bottom between elementsspace-y-2
- Vertical spacing in listsrounded-lg
- Rounded corners for modern look📱 Responsive Button Groups Design
- •
sm:
- Small screens (640px+) optimized for button groups - •
md:
- Medium screens (768px+) enhanced layout - •
lg:
- Large screens (1024px+) full functionality - • Mobile-first approach ensures button groups works on all devices
⚡ Button Groups JavaScript Logic
The Button Groups component uses a dedicated Stimulus controller (interactive-buttons-groups
) to handle button groups interactions and manage component state.
🎯 Button Groups Controller Features
Targets
DOM elements the button groups controller can reference and manipulate
Values
Configuration data for button groups behavior passed from HTML
Actions
Methods triggered by button groups user events and interactions
Lifecycle
Setup and cleanup methods for button groups initialization
🔄 Button Groups Event Flow
- 1. User interacts with button groups element (click, hover, input, etc.)
- 2. Stimulus detects event through
data-action
attribute - 3. Button Groups controller method executes with access to targets and values
- 4. Controller updates DOM with new button groups state or visual changes
- 5. CSS transitions provide smooth visual feedback for button groups interactions
<div class="space-y-4 flex flex-col items-center">
<!-- Basic Button Group -->
<div class="inline-flex rounded-lg border border-green-300 overflow-hidden" data-controller="buttons-groups" data-buttons-groups-selected-value="Left">
<button
class="px-4 py-2 bg-green-600 text-green-50 hover:bg-green-700 focus:bg-green-700 focus:z-10 focus:ring-2 focus:ring-green-400 font-medium transition-all duration-200"
data-buttons-groups-target="button"
data-value="Left"
data-action="click->buttons-groups#click"
>
Left
</button>
<button
class="px-4 py-2 bg-white border-l border-green-300 text-green-700 hover:bg-green-100 focus:bg-green-100 focus:z-10 focus:ring-2 focus:ring-green-400 font-medium transition-all duration-200"
data-buttons-groups-target="button"
data-value="Center"
data-action="click->buttons-groups#click"
>
Center
</button>
<button
class="px-4 py-2 bg-white border-l border-green-300 text-green-700 hover:bg-green-100 focus:bg-green-100 focus:z-10 focus:ring-2 focus:ring-green-400 font-medium transition-all duration-200"
data-buttons-groups-target="button"
data-value="Right"
data-action="click->buttons-groups#click"
>
Right
</button>
</div>
<!-- Toolbar Group -->
<div class="inline-flex items-center space-x-1 p-1 bg-green-100 rounded-lg" data-controller="buttons-groups" data-buttons-groups-multi-select-value="true">
<button
class="p-2 text-green-600 hover:bg-green-200 focus:bg-green-200 focus:ring-2 focus:ring-green-400 rounded-md transition-all duration-200"
data-buttons-groups-target="button"
data-value="bold"
data-action="click->buttons-groups#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-green-600 hover:bg-green-200 focus:bg-green-200 focus:ring-2 focus:ring-green-400 rounded-md transition-all duration-200"
data-buttons-groups-target="button"
data-value="italic"
data-action="click->buttons-groups#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-green-600 hover:bg-green-200 focus:bg-green-200 focus:ring-2 focus:ring-green-400 rounded-md transition-all duration-200"
data-buttons-groups-target="button"
data-value="underline"
data-action="click->buttons-groups#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-green-300 mx-1"></div>
<button
class="p-2 text-green-600 hover:bg-green-200 focus:bg-green-200 focus:ring-2 focus:ring-green-400 rounded-md transition-all duration-200"
data-buttons-groups-target="button"
data-value="link"
data-action="click->buttons-groups#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>
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["button"]
static values = {
selected: String,
multiSelect: { type: Boolean, default: false }
}
connect() {
console.log("Button group connected")
this.updateSelection()
}
click(event) {
const clickedButton = event.currentTarget
const value = clickedButton.dataset.value || clickedButton.textContent.trim()
if (this.multiSelectValue) {
this.toggleSelection(clickedButton, value)
} else {
this.selectSingle(clickedButton, value)
}
// Add ripple effect
this.addRippleEffect(event)
// Dispatch custom event
this.dispatch("selection-changed", {
detail: {
selected: this.selectedValue,
value: value,
element: clickedButton
}
})
}
selectSingle(button, value) {
// Remove active state from all buttons
this.buttonTargets.forEach(btn => {
btn.classList.remove('bg-green-600', 'text-green-50')
btn.classList.add('bg-white', 'text-green-700')
})
// Add active state to clicked button
button.classList.remove('bg-white', 'text-green-700')
button.classList.add('bg-green-600', 'text-green-50')
this.selectedValue = value
}
toggleSelection(button, value) {
const isActive = button.classList.contains('bg-green-600')
if (isActive) {
button.classList.remove('bg-green-600', 'text-green-50')
button.classList.add('bg-white', 'text-green-700')
} else {
button.classList.remove('bg-white', 'text-green-700')
button.classList.add('bg-green-600', 'text-green-50')
}
}
updateSelection() {
if (this.selectedValue) {
const selectedButton = this.buttonTargets.find(btn =>
(btn.dataset.value || btn.textContent.trim()) === this.selectedValue
)
if (selectedButton) {
this.selectSingle(selectedButton, this.selectedValue)
}
}
}
addRippleEffect(event) {
const button = event.currentTarget
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)
}
}
Split Buttons
Split buttons with dropdown menus
Resize to see how the component adapts to different screen sizes
📖 Split Buttons Overview
Split buttons with dropdown menus
💡 Key Features
- • Split Buttons functionality with Stimulus controller
- • Buttons, modals, dropdowns, and interactive UI components
- • Responsive design optimized for all screen sizes
- • Accessible markup with proper semantic HTML
- • Modern CSS transitions and interactive effects
- • Enhanced with buttons capabilities
- • Enhanced with interactive capabilities
- • Enhanced with click capabilities
- • Enhanced with primary capabilities
- • Enhanced with secondary capabilities
- • Enhanced with ghost capabilities
- • Enhanced with icon capabilities
- • Enhanced with loading capabilities
🏗️ Split Buttons HTML Structure
This split buttons component uses semantic HTML structure with Stimulus data attributes to connect HTML elements to JavaScript functionality.
📋 Stimulus Data Attributes
data-controller="interactive-buttons-split"
Connects this HTML element to the Split Buttons Stimulus controller
data-action="click->interactive-buttons-split#method"
Defines click events that trigger split buttons controller methods
data-interactive-buttons-split-target="element"
Identifies elements that the Split Buttons controller can reference and manipulate
♿ Accessibility Features
- • Semantic HTML elements provide screen reader context
- • ARIA attributes enhance split buttons accessibility
- • Keyboard navigation fully supported
- • Focus management for interactive elements
🎨 Split Buttons Tailwind Classes
This split buttons component uses Tailwind CSS utility classes with the forest theme color palette for styling and responsive design.
🎨 Interactive Elements Colors
bg-amber-600
Primary
bg-amber-100
Light
bg-honey-400
Accent
📐 Layout & Spacing
p-4
- Padding for split buttons contentmb-4
- Margin bottom between elementsspace-y-2
- Vertical spacing in listsrounded-lg
- Rounded corners for modern look📱 Responsive Split Buttons Design
- •
sm:
- Small screens (640px+) optimized for split buttons - •
md:
- Medium screens (768px+) enhanced layout - •
lg:
- Large screens (1024px+) full functionality - • Mobile-first approach ensures split buttons works on all devices
⚡ Split Buttons JavaScript Logic
The Split Buttons component uses a dedicated Stimulus controller (interactive-buttons-split
) to handle split buttons interactions and manage component state.
🎯 Split Buttons Controller Features
Targets
DOM elements the split buttons controller can reference and manipulate
Values
Configuration data for split buttons behavior passed from HTML
Actions
Methods triggered by split buttons user events and interactions
Lifecycle
Setup and cleanup methods for split buttons initialization
🔄 Split Buttons Event Flow
- 1. User interacts with split buttons element (click, hover, input, etc.)
- 2. Stimulus detects event through
data-action
attribute - 3. Split Buttons controller method executes with access to targets and values
- 4. Controller updates DOM with new split buttons state or visual changes
- 5. CSS transitions provide smooth visual feedback for split buttons interactions
<!-- 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-green-600 hover:bg-green-700 focus:bg-green-700 focus:ring-2 focus:ring-green-400 focus:ring-offset-2 text-green-50 font-medium rounded-l-lg border-r border-green-500 transition-all duration-200"
data-controller="buttons-split"
data-action="click->buttons-split#click"
>
Save Document
</button>
<button
class="px-3 py-3 bg-green-600 hover:bg-green-700 focus:bg-green-700 focus:ring-2 focus:ring-green-400 focus:ring-offset-2 text-green-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-green-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-green-700 hover:bg-green-100 focus:bg-green-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-green-700 hover:bg-green-100 focus:bg-green-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-green-200">
<button
class="flex w-full items-center px-4 py-2 text-sm text-green-700 hover:bg-green-100 focus:bg-green-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-green-700 hover:bg-green-100 focus:bg-green-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>
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
connect() {
console.log("Split button connected")
}
click(event) {
// Add ripple effect to main button
this.addRippleEffect(event)
// Show action feedback
this.showActionFeedback()
// Dispatch custom event
this.dispatch("main-action", {
detail: {
action: "save",
element: this.element
}
})
}
showActionFeedback() {
const button = this.element
const originalText = button.textContent
button.textContent = "Saving..."
button.disabled = true
setTimeout(() => {
button.textContent = "Saved!"
setTimeout(() => {
button.textContent = originalText
button.disabled = false
}, 1000)
}, 1000)
}
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)
}
}
Installation & Usage
Copy the ERB template
Copy the ERB code from the template tab above and paste it into your Rails view file.
Add the Stimulus controller
Create the Stimulus controller file in your JavaScript controllers directory.