Runtime theming
Skill almasumdev/awesome-mobile-design-system-agent-skills/.github/skills/theming/runtime-theming
In-app theme switching with persistence and high performance. Use this when users must change theme (light/dark/brand/density) without restarting the app.From its SKILL.md
npx -y skills add almasumdev/awesome-mobile-design-system-agent-skills --skill runtime-themingAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
4.8 KB, ~1.1k tokens by cl100k_base, as published. Nobody here has run it
Runtime Theming
Instructions
Runtime theming means the same process serves a different theme without restart. Make it cheap to switch and cheap to render.
1. Model the Theme as Plain Data
A theme is an immutable data object. Switching means replacing the object and letting the framework recompose.
data class AppTheme(
val colors: AppColors,
val typography: AppTypography,
val spacing: AppSpacing,
val motion: AppMotion,
val density: Density,
val isDark: Boolean,
)
No mutable fields. No callbacks inside the theme object.
2. Expose Through Platform-Idiomatic Channels
Compose: one CompositionLocal per theme axis, read in components.
val LocalAppTheme = staticCompositionLocalOf<AppTheme> { error("AppTheme not provided") }
@Composable
fun AppThemeProvider(theme: AppTheme, content: @Composable () -> Unit) {
CompositionLocalProvider(LocalAppTheme provides theme) {
MaterialTheme(colorScheme = theme.colors.toColorScheme(),
typography = theme.typography.toTypography()) { content() }
}
}
SwiftUI: @Environment keys.
private struct DSThemeKey: EnvironmentKey { static let defaultValue: DSTheme = .light }
extension EnvironmentValues { var dsTheme: DSTheme { get { self[DSThemeKey.self] } set { self[DSThemeKey.self] = newValue } } }
RootView().environment(\.dsTheme, currentTheme)
Flutter: Theme.of(context) plus a ThemeExtension for custom tokens.
React Native: a React context with useMemo on the theme object; pair with a StyleSheet factory.
3. Persist the User Choice
Store only the choice, never the resolved theme.
enum class ThemePreference { System, Light, Dark }
class ThemeRepository(private val ds: DataStore<Preferences>) {
val preference: Flow<ThemePreference> = ds.data.map {
ThemePreference.valueOf(it[KEY] ?: ThemePreference.System.name)
}
suspend fun set(pref: ThemePreference) { ds.edit { it[KEY] = pref.name } }
companion object { private val KEY = stringPreferencesKey("theme.pref") }
}
Resolve to the concrete theme at the root:
val pref by themeRepository.preference.collectAsState(initial = ThemePreference.System)
val isDark = when (pref) {
ThemePreference.System -> isSystemInDarkTheme()
ThemePreference.Dark -> true
ThemePreference.Light -> false
}
AppThemeProvider(theme = if (isDark) DarkTheme else LightTheme) { AppRoot() }
4. Performance
- Keep the theme object referentially stable. New instance on every recomposition → everything re-reads.
- Split the theme by axis if one axis (e.g., density) changes more often than another (colors). Multiple small
CompositionLocals beat one big one. - Do not wrap the whole tree in an animated color transition on theme change; let the OS drive animation via the native transition (Compose
Crossfadeat the root is acceptable at 150–200ms). - Never read theme fields inside tight loops (e.g., inside
itemsof a lazy list in a draw phase); hoist to outer composition.
5. Animating the Transition
A 150–250ms crossfade at the root is enough. Avoid per-color lerps across the tree — expensive and often janky.
Crossfade(targetState = theme, animationSpec = tween(200)) { t ->
AppThemeProvider(theme = t) { AppRoot() }
}
On iOS / SwiftUI, simply set preferredColorScheme and let UIKit drive the animation. On Flutter, AnimatedTheme is fine for light/dark; for brand switches, rebuild the root.
6. Density and Size-Class Switching
Density ("comfortable" vs "compact") and size class belong in the theme, not in per-screen state. Toggling density mid-session must not relayout the entire tree unstably — prefer a settings-gated restart if your layout is highly density-dependent.
7. Anti-Patterns
- Mutable singletons (
ThemeManager.shared.theme = .dark) — invisible to frameworks. - Recomputing the theme object on every render.
- Switching themes by replacing the root widget (
MaterialApp) — loses navigation state. - Persisting the entire theme blob to storage instead of the preference key.
- Per-component theme props; all theme access must flow through the provider.
Checklist
- The theme is an immutable data object, provided via the platform's canonical channel.
- User preference is persisted; concrete theme is resolved at the root from preference + system.
- Theme object identity is stable across unrelated recompositions.
- Switching theme does not reset navigation stack or lose input focus.
- A crossfade of 150–250ms at the root handles the transition; no per-color lerp.
- No mutable global theme state.
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.