Purpose: Controls theme switching functionality with support for light, dark, and system modes, including persistence and automatic icon updates.
Features
- Three theme modes: light, dark, and system
- Automatic persistence to localStorage
- System preference detection via
prefers-color-scheme - Automatic icon updates based on current theme
- Custom event dispatching for theme changes
- Seamless integration with Tailwind CSS dark mode
Static Values
static values = {
theme: {
type: String,
default: "system" // Initial theme mode
}
}
Targets
lightIcon: Icon element displayed in light modedarkIcon: Icon element displayed in dark modesystemIcon: Icon element displayed in system mode
Actions
toggle: Cycles through light → dark → system → light
Usage Examples
Basic Theme Button
<%= fx_theme_button %>
With Tooltip
<%= fx_theme_button(
tooltip_text: "Toggle theme",
tooltip_placement: :bottom
) %>
Custom Styling
<%= fx_theme_button(
color: :info,
size: 3,
class: "fixed top-4 right-4"
) %>
Manual HTML Structure
<button data-controller="fx-theme-button"
data-action="click->fx-theme-button#toggle"
class="rounded-full p-2">
<span class="hidden" data-fx-theme-button-target="lightIcon">
<!-- Sun icon -->
</span>
<span class="hidden" data-fx-theme-button-target="darkIcon">
<!-- Moon icon -->
</span>
<span class="hidden" data-fx-theme-button-target="systemIcon">
<!-- Computer icon -->
</span>
</button>
Theme Modes
Light Mode
- Sets
localStorage.theme = "light" - Removes
darkclass from<html>element - Shows sun icon
Dark Mode
- Sets
localStorage.theme = "dark" - Adds
darkclass to<html>element - Shows moon icon
System Mode
- Removes
localStorage.themekey - Applies theme based on
prefers-color-schememedia query - Shows computer icon
- Automatically updates when system preference changes
Event System
Listening to Theme Changes
// Listen for theme change events
document.addEventListener('fx-theme-button:changed', (event) => {
console.log('Theme changed to:', event.detail.theme);
// event.detail.theme can be: 'light', 'dark', or 'system'
});
Event Details
{
detail: {
theme: "light" | "dark" | "system"
}
}
Tailwind Dark Mode Setup
Required Configuration
// tailwind.config.js
module.exports = {
darkMode: 'class', // REQUIRED: Use class strategy
// ... other config
}
HTML Class Strategy
The controller applies the dark class to the <html> element:
<!-- Light mode -->
<html>
<!-- Dark mode -->
<html class="dark">
Persistence Behavior
localStorage Keys
theme: "light"- Light mode selectedtheme: "dark"- Dark mode selectedthemekey removed - System mode selected
Loading Saved Theme
On page load, the controller:
- Checks for saved theme in localStorage
- Falls back to checking for
darkclass on<html> - Defaults to “system” if nothing found
- Applies the theme and updates icons
Icon Management
Icon Targets
Each icon is wrapped in a <span> with the appropriate target:
<span data-fx-theme-button-target="lightIcon" class="hidden">
<!-- Sun icon (shown in light mode) -->
</span>
<span data-fx-theme-button-target="darkIcon" class="hidden">
<!-- Moon icon (shown in dark mode) -->
</span>
<span data-fx-theme-button-target="systemIcon" class="hidden">
<!-- Computer icon (shown in system mode) -->
</span>
Automatic Updates
The controller automatically:
- Hides all icons
- Shows only the icon for the current theme
- Updates on theme changes
Methods
Public Methods
// Toggle theme (cycles through modes)
controller.toggle()
// Apply specific theme
controller.applyTheme("light" | "dark" | "system")
// Get saved theme from localStorage
controller.getSavedTheme()
// Save theme to localStorage
controller.saveTheme("light" | "dark" | "system")
// Update icon visibility
controller.updateIcon()
Integration Examples
With Navigation Bar
<nav class="flex items-center justify-between p-4 bg-white dark:bg-gray-800">
<div class="logo">My App</div>
<div class="flex items-center gap-4">
<%= fx_theme_button %>
<%= fx_button(href: "/profile") { "Profile" } %>
</div>
</nav>
With User Preferences
// Listen for theme changes and save to user preferences
document.addEventListener('fx-theme-button:changed', (event) => {
fetch('/api/user/preferences', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ theme: event.detail.theme })
});
});
Programmatic Theme Control
// Get the controller instance
const element = document.querySelector('[data-controller="fx-theme-button"]');
const app = Stimulus.Application.start();
const controller = app.getControllerForElementAndIdentifier(element, 'fx-theme-button');
// Set theme programmatically
controller.applyTheme('dark');
controller.saveTheme('dark');
controller.updateIcon();
System Preference Detection
Media Query
The controller uses the prefers-color-scheme media query in system mode:
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
html.classList.add("dark");
} else {
html.classList.remove("dark");
}
Automatic Updates
In system mode, the theme automatically updates when:
- Page loads with system mode selected
- User manually selects system mode
- (Note: Real-time OS theme changes require additional listeners)
Accessibility
ARIA Labels (Optional Enhancement)
<button data-controller="fx-theme-button"
data-action="click->fx-theme-button#toggle"
aria-label="Toggle theme">
<!-- Icons -->
</button>
Keyboard Support
- Works with standard button keyboard navigation
- Activates with Enter or Space key
- Focusable with Tab key
Best Practices
- Place in persistent layouts (navbar, header) for consistent access
- Provide tooltips for better UX
- Test with system dark mode to ensure proper detection
- Use semantic colors that work in both light and dark modes
- Consider user preference persistence across sessions
- Test icon visibility in all three modes
Common Use Cases
- Application-wide theme switching
- User preference settings
- Dark mode toggle in navigation
- Theme selector in settings panels
- Reading mode toggles
- Accessibility improvements
Troubleshooting
Dark mode not applying: Ensure tailwind.config.js has darkMode: 'class' Icons not updating: Check that all three icon targets exist Theme not persisting: Verify localStorage is available and not blocked System mode not working: Test prefers-color-scheme media query support Multiple buttons conflicting: Each button operates independently on the same global theme
Browser Support
- localStorage: All modern browsers
- prefers-color-scheme: Chrome 76+, Firefox 67+, Safari 12.1+
- Stimulus: Works with Stimulus 3.x
- Tailwind Dark Mode: Tailwind CSS 2.0+
Related Components
- ThemeButton Component - The Ruby component that uses this controller
- Button Component - Base button component