Skip to main content
Lumo UI generates a fully customizable theme system for your Compose application. This guide explains how to customize colors, typography, and other theme properties.

Theme Structure

The generated theme consists of three main components:
  1. Colors - Color palette for light and dark modes
  2. Typography - Text styles and font definitions
  3. Theme Composable - Main theme wrapper with configuration

Color Customization

Color System

Lumo UI uses a comprehensive color system with support for both light and dark modes. The Colors data class defines all color tokens:
Color.kt
data class Colors(
    val primary: Color,
    val onPrimary: Color,
    val secondary: Color,
    val onSecondary: Color,
    val tertiary: Color,
    val onTertiary: Color,
    val error: Color,
    val onError: Color,
    val success: Color,
    val onSuccess: Color,
    val disabled: Color,
    val onDisabled: Color,
    val surface: Color,
    val onSurface: Color,
    val background: Color,
    val onBackground: Color,
    val outline: Color,
    val transparent: Color = Color.Transparent,
    val white: Color = White,
    val black: Color = Black,
    val text: Color,
    val textSecondary: Color,
    val textDisabled: Color,
    val scrim: Color,
    val elevation: Color,
)

Predefined Color Palettes

Lumo UI includes predefined color values organized by color family:
val Black: Color = Color(0xFF000000)
val Gray900: Color = Color(0xFF282828)
val Gray800: Color = Color(0xFF4b4b4b)
val Gray700: Color = Color(0xFF5e5e5e)
val Gray600: Color = Color(0xFF727272)
val Gray500: Color = Color(0xFF868686)
val Gray400: Color = Color(0xFFC7C7C7)
val Gray300: Color = Color(0xFFDFDFDF)
val Gray200: Color = Color(0xFFE2E2E2)
val Gray100: Color = Color(0xFFF7F7F7)
val Gray50: Color = Color(0xFFFFFFFF)
val White: Color = Color(0xFFFFFFFF)

Light and Dark Color Schemes

Define separate color schemes for light and dark modes:
internal val LightColors = Colors(
    primary = Black,
    onPrimary = White,
    secondary = Gray400,
    onSecondary = Black,
    tertiary = Blue900,
    onTertiary = White,
    surface = Gray200,
    onSurface = Black,
    error = Red600,
    onError = White,
    success = Green600,
    onSuccess = White,
    disabled = Gray300,
    onDisabled = Gray500,
    background = White,
    onBackground = Black,
    outline = Gray300,
    transparent = Color.Transparent,
    white = White,
    black = Black,
    text = Black,
    textSecondary = Gray700,
    textDisabled = Gray400,
    scrim = Color.Black.copy(alpha = 0.32f),
    elevation = Gray500,
)

Customizing Colors

To customize colors, edit the LightColors and DarkColors definitions in your Color.kt file:
Custom Brand Colors
// Add your brand colors
val BrandPrimary: Color = Color(0xFF1E88E5)
val BrandSecondary: Color = Color(0xFFFFC107)
val BrandAccent: Color = Color(0xFFE91E63)

// Update the color scheme
internal val LightColors = Colors(
    primary = BrandPrimary,
    onPrimary = White,
    secondary = BrandSecondary,
    onSecondary = Black,
    tertiary = BrandAccent,
    onTertiary = White,
    // ... rest of colors
)

Accessing Colors in Components

Access theme colors in your composables using the theme object:
Using Theme Colors
@Composable
fun MyComponent() {
    Box(
        modifier = Modifier
            .background(AppTheme.colors.background)
            .padding(16.dp)
    ) {
        Text(
            text = "Hello World",
            color = AppTheme.colors.text
        )
    }
}

Typography Customization

Typography System

Lumo UI provides a comprehensive typography system with 12 text styles:
Typography.kt
data class Typography(
    val h1: TextStyle,      // Large headings
    val h2: TextStyle,      // Medium headings
    val h3: TextStyle,      // Small headings
    val h4: TextStyle,      // Extra small headings
    val body1: TextStyle,   // Primary body text
    val body2: TextStyle,   // Secondary body text
    val body3: TextStyle,   // Tertiary body text
    val label1: TextStyle,  // Large labels
    val label2: TextStyle,  // Medium labels
    val label3: TextStyle,  // Small labels
    val button: TextStyle,  // Button text
    val input: TextStyle,   // Input field text
)

Default Text Styles

Here are the default text style definitions:
Default Typography
private val defaultTypography = Typography(
    h1 = TextStyle(
        fontWeight = FontWeight.Bold,
        fontSize = 24.sp,
        lineHeight = 32.sp,
        letterSpacing = 0.sp,
    ),
    h2 = TextStyle(
        fontWeight = FontWeight.Bold,
        fontSize = 20.sp,
        lineHeight = 28.sp,
        letterSpacing = 0.sp,
    ),
    h3 = TextStyle(
        fontWeight = FontWeight.Bold,
        fontSize = 16.sp,
        lineHeight = 24.sp,
        letterSpacing = 0.sp,
    ),
    h4 = TextStyle(
        fontWeight = FontWeight.SemiBold,
        fontSize = 16.sp,
        lineHeight = 24.sp,
        letterSpacing = 0.sp,
    ),
    body1 = TextStyle(
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp,
        lineHeight = 24.sp,
        letterSpacing = 0.sp,
    ),
    body2 = TextStyle(
        fontWeight = FontWeight.Normal,
        fontSize = 14.sp,
        lineHeight = 20.sp,
        letterSpacing = 0.15.sp,
    ),
    body3 = TextStyle(
        fontWeight = FontWeight.Normal,
        fontSize = 12.sp,
        lineHeight = 16.sp,
        letterSpacing = 0.15.sp,
    ),
    label1 = TextStyle(
        fontWeight = FontWeight.W500,
        fontSize = 14.sp,
        lineHeight = 20.sp,
        letterSpacing = 0.1.sp,
    ),
    label2 = TextStyle(
        fontWeight = FontWeight.W500,
        fontSize = 12.sp,
        lineHeight = 16.sp,
        letterSpacing = 0.5.sp,
    ),
    label3 = TextStyle(
        fontWeight = FontWeight.W500,
        fontSize = 10.sp,
        lineHeight = 12.sp,
        letterSpacing = 0.5.sp,
    ),
    button = TextStyle(
        fontWeight = FontWeight.W500,
        fontSize = 14.sp,
        lineHeight = 20.sp,
        letterSpacing = 1.sp,
    ),
    input = TextStyle(
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp,
        lineHeight = 24.sp,
        letterSpacing = 0.sp,
    ),
)

Custom Font Family

To use a custom font family, define it in your Typography file:
@Composable
fun fontFamily() = FontFamily(
    Font(Res.font.poppins_black, FontWeight.Black),
    Font(Res.font.poppins_extrabold, FontWeight.ExtraBold),
    Font(Res.font.poppins_bold, FontWeight.Bold),
    Font(Res.font.poppins_semibold, FontWeight.SemiBold),
    Font(Res.font.poppins_medium, FontWeight.Medium),
    Font(Res.font.poppins_regular, FontWeight.Normal),
    Font(Res.font.poppins_light, FontWeight.Light),
    Font(Res.font.poppins_extralight, FontWeight.ExtraLight),
    Font(Res.font.poppins_thin, FontWeight.Thin),
)

Applying Font Family to Typography

Apply your custom font family to all text styles:
provideTypography
@Composable
fun provideTypography(): Typography {
    val fontFamily = fontFamily()

    return defaultTypography.copy(
        h1 = defaultTypography.h1.copy(fontFamily = fontFamily),
        h2 = defaultTypography.h2.copy(fontFamily = fontFamily),
        h3 = defaultTypography.h3.copy(fontFamily = fontFamily),
        h4 = defaultTypography.h4.copy(fontFamily = fontFamily),
        body1 = defaultTypography.body1.copy(fontFamily = fontFamily),
        body2 = defaultTypography.body2.copy(fontFamily = fontFamily),
        body3 = defaultTypography.body3.copy(fontFamily = fontFamily),
        label1 = defaultTypography.label1.copy(fontFamily = fontFamily),
        label2 = defaultTypography.label2.copy(fontFamily = fontFamily),
        label3 = defaultTypography.label3.copy(fontFamily = fontFamily),
        button = defaultTypography.button.copy(fontFamily = fontFamily),
        input = defaultTypography.input.copy(fontFamily = fontFamily),
    )
}

Using Typography in Components

Access typography styles through the theme:
Using Typography
@Composable
fun MyComponent() {
    Column {
        Text(
            text = "Heading",
            style = AppTheme.typography.h1
        )
        Text(
            text = "Body text",
            style = AppTheme.typography.body1
        )
        Button(
            onClick = { /* ... */ }
        ) {
            Text(
                text = "Click Me",
                style = AppTheme.typography.button
            )
        }
    }
}

Font Scaling

Lumo UI supports dynamic font scaling for accessibility:
Font Scaling
fun scaledTypography(fontScale: Float, default: Typography): Typography {
    return Typography(
        h1 = default.h1.copy(
            fontSize = 24.sp * fontScale,
            lineHeight = 32.sp * fontScale
        ),
        h2 = default.h2.copy(
            fontSize = 20.sp * fontScale,
            lineHeight = 28.sp * fontScale
        ),
        // ... other styles
    )
}

Theme Composable

The main theme composable wraps your app content and provides theme values:
Theme.kt
@Composable
fun AppTheme(
    isDarkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit,
) {
    val rippleIndication = ripple()
    val selectionColors = rememberTextSelectionColors(LightColors)

    val appConfigState = rememberAppConfigState()
    val defaultTypography = provideTypography()
    val scaledTypography = scaledTypography(appConfigState.fontScale, defaultTypography)
    val colors = if (isDarkTheme) appConfigState.colors.darkColors else appConfigState.colors.lightColors

    CompositionLocalProvider(
        LocalColors provides colors,
        LocalDefaultColors provides AppColors(),
        LocalContentColor provides colors.contentColorFor(colors.background),
        LocalTypography provides scaledTypography,
        LocalOriginalTypography provides scaledTypography,
        LocalIndication provides rippleIndication,
        LocalTextSelectionColors provides selectionColors,
        LocalAppConfigState provides appConfigState,
        LocalTextStyle provides scaledTypography.body1,
        LocalLayoutDirection provides appConfigState.layoutDirection,
        content = content,
    )
}

Using the Theme

Wrap your app content with the theme composable:
Application Setup
@Composable
fun MyApp() {
    AppTheme {
        // Your app content
        MainScreen()
    }
}

// Force dark mode
@Composable
fun MyAppDarkMode() {
    AppTheme(isDarkTheme = true) {
        MainScreen()
    }
}

Dynamic Theme Updates

You can create dynamic theme updates by modifying the AppColors data class:
Dynamic Colors
data class AppColors(
    val lightColors: Colors = LightColors,
    val darkColors: Colors = DarkColors,
)

// In your app state
val customColors = remember {
    mutableStateOf(
        AppColors(
            lightColors = LightColors.copy(primary = Color.Blue),
            darkColors = DarkColors.copy(primary = Color.Cyan)
        )
    )
}

Best Practices

Always use semantic color tokens (like primary, error, surface) instead of hardcoding colors. This ensures consistency and makes theme switching easier.
// Good
Text(color = AppTheme.colors.primary)

// Avoid
Text(color = Color(0xFF1E88E5))
Ensure sufficient contrast between colors, especially for text. Use the onX color tokens for text on colored backgrounds:
Box(
    modifier = Modifier.background(AppTheme.colors.primary)
) {
    Text(color = AppTheme.colors.onPrimary)
}
Always test your UI in both light and dark modes to ensure colors work well in both contexts.
Stick to the predefined typography styles for consistency. Only create custom styles when absolutely necessary.

Next Steps

lumo.properties

Configure the plugin settings

Directory Structure

Understand the project structure

Build docs developers (and LLMs) love