Theme Guide
MIDDAG React UI includes a complete theme system with dark mode, host theme detection, and RTL support. The theme adapts automatically to the host application (Moodle or WordPress) or can be controlled manually.
Appearance Modes
The theme system supports three appearance modes:
- system -- auto-detects from the host application (Moodle/WP) or falls back to OS
prefers-color-scheme. - light -- forces light mode regardless of host or OS preference.
- dark -- forces dark mode regardless of host or OS preference.
Users can override via the appearance toggle in the sidebar footer, which cycles through System > Light > Dark > System.
Appearance API
Import these functions from @middag-io/react to control the theme programmatically:
| Function | Signature | Description |
|---|---|---|
getStoredAppearance() | () => Appearance | Read the stored preference from localStorage. Returns "system", "light", or "dark". |
setAppearance(pref) | (Appearance) => void | Persist preference and apply the resolved theme to DOM immediately. |
cycleAppearance() | () => Appearance | Cycle to the next mode (system > light > dark > system) and return the new value. |
getEffectiveTheme(pref) | (Appearance) => "light" | "dark" | Resolve a preference to the effective theme. For "system", consults host then OS. |
applyTheme(theme) | ("light" | "dark") => void | Apply a theme to DOM: sets data-theme on .middag-root elements and toggles .dark on <html>. |
toggleDir() | () => "ltr" | "rtl" | Toggle document direction between LTR and RTL. Persists choice in localStorage. |
initDir() | () => void | Restore persisted direction on page load. |
onSystemThemeChange(cb?) | (cb?: () => void) => () => void | Listen for OS theme changes. Only reacts when preference is 'system'. Returns cleanup function. |
Example
import { cycleAppearance, getStoredAppearance } from '@middag-io/react';
function ThemeToggle() {
const [mode, setMode] = useState(getStoredAppearance());
return (
<button onClick={() => setMode(cycleAppearance())}>
{mode === 'system' ? 'System' : mode === 'light' ? 'Light' : 'Dark'}
</button>
);
}useIsDark Hook
The useIsDark hook provides a reactive boolean that updates whenever the theme changes. It uses useSyncExternalStore with a MutationObserver on <html> to detect class changes:
import { useIsDark } from '@middag-io/react';
function MyComponent() {
const isDark = useIsDark();
return (
<div style={{ background: isDark ? '#1a1a1a' : '#ffffff' }}>
Current theme: {isDark ? 'dark' : 'light'}
</div>
);
}Prefer Tailwind classes
In most cases you can use Tailwind dark: prefix classes instead of useIsDark. The hook is mainly useful when you need to pass theme information to third-party libraries or canvas-based components.
Host Theme Detection
When appearance is set to system, the theme engine detects the host application's theme before falling back to OS preference:
- Moodle -- checks
<html data-theme>,.theme-darkclass, and Boost CSS variables. - WordPress -- checks
body.admin-color-*classes and<html class="dark">. - Fallback -- OS
prefers-color-scheme: darkmedia query.
Read-only detection
Host theme detection is read-only. MIDDAG never modifies the host page's own classes or attributes -- it only reads them to determine the current theme.
Design Tokens
The design system uses OKLCH colors for perceptual uniformity:
| Token | Value | Description |
|---|---|---|
| Primary | oklch(0.21 0.034 264.06) | Main brand color for text and borders. |
| Accent | oklch(0.58 0.16 250) | Interactive elements, links, and focus rings. |
| Success | semantic | Positive states and confirmations. |
| Warning | semantic | Caution states and non-blocking alerts. |
| Destructive | semantic | Errors, deletions, and breaking actions. |
| Info | semantic | Informational callouts and neutral highlights. |
Dark mode applies surface inversion with saturation reduction for comfortable reading.
Use Tailwind semantic classes
Use Tailwind semantic utility classes like text-foreground, bg-background, and border -- they auto-switch between light and dark mode without any extra logic.
RTL Support
Full right-to-left support is built in:
toggleDir()switches between LTR and RTL, persisting in localStorage.initDir()restores persisted direction on page load.- Tailwind RTL utilities (
rtl:prefix) work automatically.
import { toggleDir, initDir } from '@middag-io/react';
// On app load, restore persisted direction
initDir();
// Toggle on user action
const newDir = toggleDir();
console.log('Direction:', newDir); // 'rtl' or 'ltr'Types
type Appearance = 'system' | 'light' | 'dark';
type EffectiveTheme = 'light' | 'dark';
type AsyncStringResolver = (key: string, component?: string) => Promise<string>;