Custom Theme Systems¶
Use Halogen's LLM-generated themes with any design system - not just Material 3.
Architecture: M3-Agnostic Core¶
Halogen's core is intentionally decoupled from Material 3. The halogen-core module outputs:
HalogenColorScheme- 49 color roles as ARGB integersHalogenTypography- font mood, heading/body weights, letter spacingHalogenShapes- 5 corner radius sizes in dpExpandedTheme- bundles all of the above (light + dark schemes)
None of these types reference Material 3. The M3 integration lives entirely in halogen-compose, which is optional.
Direct Usage with ThemeExpander¶
If you only need the expanded theme data (no Compose UI):
val spec: HalogenThemeSpec = ... // from LLM, cache, or server
val expanded = ThemeExpander.expand(spec, HalogenConfig.Default)
// Access raw color values (ARGB ints)
val primaryColor = expanded.lightColorScheme.primary // e.g., 0xFF1A73E8
val darkSurface = expanded.darkColorScheme.surface // e.g., 0xFF1C1B1F
// Access typography
val fontHint = expanded.typography.fontFamilyHint() // e.g., "sans-serif"
val headingWeight = expanded.typography.headingWeight // e.g., 700
// Access shapes
val cornerRadius = expanded.shapes.medium // e.g., 16.0f dp
Mapping to a Custom Theme¶
Here's a concrete example mapping Halogen's output to a hypothetical company design system:
// Your company's theme data class
data class AcmeTheme(
val brandPrimary: Color,
val brandSecondary: Color,
val textPrimary: Color,
val textSecondary: Color,
val backgroundMain: Color,
val backgroundCard: Color,
val borderDefault: Color,
val cornerRadius: Dp,
val headingFontWeight: FontWeight,
val bodyFontWeight: FontWeight,
)
// Map ExpandedTheme to your theme
fun ExpandedTheme.toAcmeTheme(isDark: Boolean): AcmeTheme {
val colors = if (isDark) darkColorScheme else lightColorScheme
return AcmeTheme(
brandPrimary = Color(colors.primary),
brandSecondary = Color(colors.secondary),
textPrimary = Color(colors.onSurface),
textSecondary = Color(colors.onSurfaceVariant),
backgroundMain = Color(colors.surface),
backgroundCard = Color(colors.surfaceContainer),
borderDefault = Color(colors.outlineVariant),
cornerRadius = shapes.medium.dp,
headingFontWeight = FontWeight(typography.headingWeight),
bodyFontWeight = FontWeight(typography.bodyWeight),
)
}
Using HalogenTheme with a Custom Wrapper¶
HalogenTheme supports a themeWrapper parameter that lets you replace the default MaterialTheme wrapping with your own:
// Composition local for your custom theme
val LocalAcmeTheme = staticCompositionLocalOf { AcmeTheme.defaults() }
HalogenTheme(
spec = currentSpec,
themeWrapper = { expanded, isDark, content ->
val acmeTheme = remember(expanded, isDark) {
expanded.toAcmeTheme(isDark)
}
CompositionLocalProvider(
LocalAcmeTheme provides acmeTheme,
) {
content()
}
},
) {
// Inside here, access your theme:
val theme = LocalAcmeTheme.current
Text("Hello", color = theme.textPrimary)
}
When themeWrapper is provided, HalogenTheme handles the spec expansion and dark mode switching, but delegates the actual theme application to your wrapper. No MaterialTheme is applied.
Extensions with Custom Themes¶
Custom extensions work the same way regardless of whether you use Material 3 or a custom theme system. Register them via the engine builder:
val halogen = Halogen.Builder()
.provider(myProvider)
.extensions(
HalogenExtension("brandAccent", "A vibrant accent for your brand"),
HalogenExtension("successGreen", "Success state color"),
)
.build()
Access them via HalogenTheme.extensions:
Which Modules Do You Need?¶
| Use Case | Modules |
|---|---|
| Material 3 theme (standard) | halogen-core + halogen-engine + halogen-compose |
| Custom theme system in Compose | halogen-core + halogen-engine + halogen-compose |
| Non-Compose (raw theme data) | halogen-core + halogen-engine |
| Color science only | halogen-core |
halogen-compose is needed even for custom themes if you want the HalogenTheme composable with themeWrapper. If you're building outside Compose entirely, use halogen-core + halogen-engine and consume ExpandedTheme directly.