Theme System
Last updated: 04/17/2026 · Written by Agent0
Theme System & theme.css
StackCTL ships with a lightweight theme system that handles light/dark mode switching, stores the user's preference, and fires a theme:ready event that other scripts (like TinyMCE) can listen for. It also includes theme.css — a small set of reusable UI classes that sit alongside Tailwind and reduce repetition in views.
Both are loaded automatically in layouts/base.php — no configuration required.
How the Theme System Works
The theme system runs in two stages on every page load:
- Instant flash prevention — An inline script in
<head>reads the stored preference fromlocalStorageand applies thedarkclass to<html>before the page renders. This prevents a white flash on dark mode loads. - Full initialization — After the page loads, the
window.Themecontroller initializes, wires up the toggle button, listens for OS-level preference changes, and fires thetheme:readyevent.
Tailwind's dark mode is configured to use the class strategy, so any Tailwind dark: utility will respond to the dark class on <html> automatically.
Three Theme Modes
The theme cycles through three states:
- Light — Forces light mode regardless of OS setting. Icon: ☀️
- Dark — Forces dark mode regardless of OS setting. Icon: 🌙
- System — Follows the operating system preference. Icon: 🖥️
The preference is stored in localStorage under the key 'theme' and persists across sessions.
The Toggle Button
The theme toggle button in the top navigation has the id themeToggle. Clicking it cycles through the three modes automatically — no additional JavaScript needed. The button label updates to reflect the current mode.
<button id="themeToggle" class="theme-btn">🖥️</button>
Manual Theme Buttons
To build a settings panel with explicit Light / Dark / System buttons, use data-theme attributes. The theme system will automatically apply the theme-btn-active class to whichever button matches the current mode.
<button class="theme-btn" data-theme="light">Light</button>
<button class="theme-btn" data-theme="dark">Dark</button>
<button class="theme-btn" data-theme="system">System</button>
<script>
document.querySelectorAll('[data-theme]').forEach(btn => {
btn.addEventListener('click', () => {
Theme.set(btn.dataset.theme);
});
});
</script>
The Theme JavaScript API
The window.Theme object is available globally on every page. You can call it from any view script.
Theme.get() // Returns current mode: 'light', 'dark', or 'system'
Theme.isDark() // Returns true if the UI is currently in dark mode
Theme.set('dark') // Set a specific mode ('light', 'dark', 'system')
Theme.cycle() // Advance to the next mode (light → dark → system → light)
Theme.getTinyMCEConfig() // Returns the correct TinyMCE skin/content_css for the current mode
theme:ready event
The theme:ready event fires after Theme.init() completes. Use it as the entry point for any script that needs the theme to be fully loaded first — most importantly, TinyMCE initialization.
document.addEventListener('theme:ready', () => {
// Theme is fully loaded and Theme.isDark() is reliable here
console.log('Dark mode:', Theme.isDark());
});
See the TinyMCE doc for a full example of using theme:ready with the editor.
theme.css — UI Class Reference
theme.css provides a small set of semantic CSS classes that work on top of Tailwind. They handle light/dark mode internally, so you don't have to repeat dark: variants on every element. Use them in views exactly like Tailwind classes.
Buttons
<button class="btn btn-primary">Save</button> <button class="btn btn-secondary">Cancel</button> <button class="btn btn-danger">Delete</button> <button class="btn btn-primary" disabled>Disabled</button>
btn— Base button style (required on all buttons)btn-primary— Blue, for primary actionsbtn-secondary— Light gray, for secondary actions and cancel buttonsbtn-danger— Red, for destructive actions like delete
Form Inputs
<input type="text" class="input" name="title"> <input type="text" class="input input-error" name="email"> <!-- validation failure --> <textarea class="input" name="content"></textarea>
input— Full-width input with border, padding, focus ring, and dark mode supportinput-error— Red border variant, applied automatically byhas_error()when a validation error exists
Labels
<label class="label">Article Title</label>
label— Block-level label with appropriate font size, weight, and bottom margin
Cards
<div class="card">
<!-- your content -->
</div>
card— White background, rounded corners, subtle shadow. Switches to dark gray in dark mode automatically.
Grid / Row System
A lightweight responsive grid that doesn't require writing breakpoint classes on every element.
<!-- Single column (default, stacks on mobile) -->
<div class="row row-2">
<div>Column one</div>
<div>Column two</div>
</div>
<!-- Three columns -->
<div class="row row-3"> ... </div>
<!-- Auto-fit tiles (responsive, no fixed column count) -->
<div class="row-auto"> ... </div>
row— Base grid withgap-6, single column on mobilerow-2throughrow-6— Number of columns on tablet and aboverow-auto— Auto-fit responsive tiles, minimum 200px wide
Navigation
<a href="/articles" class="nav-link <?= is_active('/articles') ?>">Articles</a>
<div class="nav-section">
<span class="nav-section-label">Content</span>
<a href="/articles" class="nav-link">Articles</a>
<a href="/categories" class="nav-link">Categories</a>
</div>
nav-link— Block link with hover background, works in light and dark modenav-section— Adds a top border separator and spacing between nav groupsnav-section-label— Small uppercase gray label for grouping nav items
Combine nav-link with the is_active() helper to highlight the current page. See the Views & Layouts doc for usage details.
Alerts
<div class="alert alert-success">Changes saved successfully.</div> <div class="alert alert-error">Something went wrong.</div>
alert— Base alert style with padding and rounded cornersalert-success— Green tones, dark mode awarealert-error— Red tones, dark mode aware
Note: flash toast notifications (the auto-dismissing popups) are handled automatically by the base layout using Tailwind classes. The alert classes are for inline alerts you place manually in a view.
Text & Spacing Utilities
<p class="text-muted">Supporting copy or helper text.</p> <div class="mt-sm">...</div> <!-- margin-top: 0.5rem --> <div class="mt-md">...</div> <!-- margin-top: 1rem --> <div class="mt-lg">...</div> <!-- margin-top: 1.5rem -->
text-muted— Medium gray in light mode, lighter gray in dark mode. Use for secondary or supporting text.mt-sm,mt-md,mt-lg— Simple margin-top helpers for quick spacing
Extending theme.css
theme.css is intentionally minimal. If you find yourself repeating a Tailwind pattern frequently across views, that's a good signal to add a new class here. Keep new additions focused on reusable primitives — layout, form elements, and containers — rather than page-specific styles.
All custom styles for your app should go into theme.css or a separate stylesheet you load in base.php. Avoid inline <style> blocks in view files.