Popular Searches
Popular Course Categories
Popular Courses

Applying Light Mode, Dark Mode, and Dynamic Colors

Applying Light Mode, Dark Mode, and Dynamic Colors

Material Design and Advanced UI Components

Applying Light Mode, Dark Mode, and Dynamic Colors in Android

Course: Android App Development

Topic: Applying Light Mode, Dark Mode, and Dynamic Colors

Course Link: Android App Development Course

Demo Link: Register for a Course Demo

JustAcademy Homepage: Visit JustAcademy


1. Introduction

Light mode, dark mode, and dynamic colors are important features of modern Android application design. They allow applications to adapt to different lighting conditions, user preferences, device settings, and visual styles. A well-designed theme improves readability, accessibility, consistency, and the overall user experience.

In Android development, Jetpack Compose and Material Design 3 provide built-in APIs for creating light and dark color schemes and applying dynamic colors. Instead of assigning fixed colors to every component, developers can define a centralized theme and allow UI components to use the appropriate colors automatically.

For example, a learning application can display dark text on a light background during the day, use light text on dark surfaces at night, and optionally derive its accent colors from the user's wallpaper on supported Android devices.

Material 3 supports light and dark color schemes across supported Android versions. Dynamic color is available on Android 12 and later, with a custom color scheme used as a fallback when dynamic color is unavailable or disabled by the application.

2. Learning Objectives

  • Understand light mode, dark mode, and dynamic colors.
  • Learn how Material Design 3 manages color schemes.
  • Configure light and dark themes using Jetpack Compose.
  • Detect the device's system theme using isSystemInDarkTheme().
  • Implement dynamic colors using dynamicLightColorScheme() and dynamicDarkColorScheme().
  • Create a theme that supports different Android versions.
  • Build a manual theme-selection control.
  • Understand semantic color roles and accessibility considerations.
  • Test themes across devices, screens, and system settings.

3. Understanding Light Mode

Light mode generally uses light backgrounds, dark text, and accent colors for important actions. It is commonly used in productivity applications, educational platforms, dashboards, and reading interfaces.

3.1 Characteristics of Light Mode

  • Light or white backgrounds.
  • Dark text for readable content.
  • Clearly visible buttons, cards, and input fields.
  • Accent colors for primary actions and selected states.
  • Neutral surfaces that help separate sections of content.

3.2 Advantages of Light Mode

  • Often works well in bright environments.
  • Can make text-heavy screens familiar and easy to scan.
  • Provides a clear visual distinction between content and surrounding surfaces.
  • Supports a broad range of application styles.

3.3 Example of Light Mode

private val LightColors = lightColorScheme(
    primary = Color(0xFF315DA8),
    onPrimary = Color.White,
    primaryContainer = Color(0xFFD9E2FF),
    onPrimaryContainer = Color(0xFF001A41),
    background = Color(0xFFFAF9FF),
    onBackground = Color(0xFF191B20),
    surface = Color(0xFFFAF9FF),
    onSurface = Color(0xFF191B20),
    error = Color(0xFFBA1A1A)
)

This example defines selected semantic colors for a custom light theme. A production application should define a complete, coordinated color scheme, including the color roles needed by its components. The sample values are illustrative and should be checked for suitable contrast in the actual interface.

4. Understanding Dark Mode

Dark mode uses dark backgrounds and surfaces with lighter foreground content. It can be helpful in low-light environments and may reduce display power consumption on some OLED devices, particularly when large portions of the screen are very dark.

4.1 Characteristics of Dark Mode

  • Dark backgrounds and content surfaces.
  • Light text and icons for readability.
  • Adjusted accent colors that remain visible against dark surfaces.
  • Distinct error, selected, disabled, and interactive states.
  • Appropriate separation between cards, dialogs, and background areas.

4.2 Advantages of Dark Mode

  • Can be more comfortable for some users in low-light environments.
  • Offers a visual preference that many users expect in modern applications.
  • May reduce power consumption on OLED screens depending on brightness and screen content.
  • Can provide a suitable appearance for media, coding, and nighttime usage.

4.3 Example of Dark Mode

private val DarkColors = darkColorScheme(
    primary = Color(0xFFB1C5FF),
    onPrimary = Color(0xFF002E69),
    primaryContainer = Color(0xFF16458B),
    onPrimaryContainer = Color(0xFFD9E2FF),
    background = Color(0xFF111318),
    onBackground = Color(0xFFE2E2E9),
    surface = Color(0xFF111318),
    onSurface = Color(0xFFE2E2E9),
    error = Color(0xFFFFB4AB)
)

The dark scheme uses different tones from the light scheme so that controls and text remain distinguishable. Avoid simply inverting every color because some accent colors require adjustment to remain readable against dark surfaces.

5. Difference Between Light Mode and Dark Mode

Feature Light Mode Dark Mode
Background Usually light Usually dark
Text Usually dark Usually light
Accent colors Selected for light surfaces Adjusted for dark surfaces
Low-light use May appear bright May feel more comfortable to some users
OLED power use Depends on brightness and content Can use less power when displaying dark pixels
Implementation Uses a light color scheme Uses a dark color scheme

6. What Are Dynamic Colors?

Dynamic color is a personalization feature associated with Material You and Material Design 3. On supported Android versions, an application can derive a coordinated color scheme from the user's wallpaper. The resulting colors can be applied to buttons, text, cards, navigation elements, and other themed components.

Dynamic colors allow an application's appearance to feel more connected to the device's visual settings without requiring developers to manually create a separate palette for every wallpaper.

6.1 Key Features of Dynamic Colors

  • Derives a coordinated color palette from the user's wallpaper on supported devices.
  • Provides dynamic light and dark color schemes.
  • Works with Material 3 components that use the active theme.
  • Supports a more personalized visual experience.
  • Requires a fallback theme for unsupported Android versions or when dynamic theming is not used.

6.2 Dynamic Color Availability

Jetpack Compose's Material 3 dynamic color APIs are available on Android 12 and later, corresponding to API level 31 and above. On earlier Android versions, use a custom light or dark color scheme.

Android Version Recommended Approach
Android 11 and earlier Use your application's custom light or dark color scheme.
Android 12 and later Use dynamic colors when enabled and appropriate, with custom schemes as fallbacks.

7. Setting Up Material 3

Before implementing themes, ensure that the Android project uses Jetpack Compose and includes a compatible Material 3 dependency.

dependencies {
    implementation("androidx.compose.material3:material3:<compatible-version>")
}

Replace <compatible-version> with a version compatible with your project's Compose setup. In projects using a version catalog, declare and manage the dependency through the catalog instead of duplicating version values.

Material 3 provides APIs such as:

  • MaterialTheme — applies a theme to composable content.
  • lightColorScheme() — creates a light color scheme.
  • darkColorScheme() — creates a dark color scheme.
  • isSystemInDarkTheme() — observes whether the system currently uses a dark theme.
  • dynamicLightColorScheme(context) — obtains the dynamic light scheme on supported Android versions.
  • dynamicDarkColorScheme(context) — obtains the dynamic dark scheme on supported Android versions.

8. Creating a Light and Dark Theme

The first step is to define separate color schemes. The application can then select the appropriate scheme based on the current theme preference.

8.1 Define the Color Schemes

// Color.kt
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.ui.graphics.Color

private val LightColors = lightColorScheme(
    primary = Color(0xFF315DA8),
    onPrimary = Color.White,
    background = Color(0xFFFAF9FF),
    onBackground = Color(0xFF191B20),
    surface = Color(0xFFFAF9FF),
    onSurface = Color(0xFF191B20)
)

private val DarkColors = darkColorScheme(
    primary = Color(0xFFB1C5FF),
    onPrimary = Color(0xFF002E69),
    background = Color(0xFF111318),
    onBackground = Color(0xFFE2E2E9),
    surface = Color(0xFF111318),
    onSurface = Color(0xFFE2E2E9)
)

These are simplified examples of custom schemes. Add any other required color roles, such as primary containers, secondary colors, outlines, and error colors, to suit the application's components and design system.

8.2 Apply the Theme Using System Settings

// Theme.kt
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable

@Composable
fun AppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val colorScheme = if (darkTheme) {
        DarkColors
    } else {
        LightColors
    }

    MaterialTheme(
        colorScheme = colorScheme,
        content = content
    )
}

8.3 Apply the Theme in MainActivity

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            AppTheme {
                HomeScreen()
            }
        }
    }
}

In this example, HomeScreen() represents your application's main composable. Make sure the required Android and Compose imports are included. When the system theme changes, Compose can update the theme because isSystemInDarkTheme() reads the current system setting.

9. Implementing Dynamic Colors

To support dynamic colors, check whether the Android version is Android 12 or later. If it is supported, choose the dynamic light or dark color scheme. Otherwise, use the custom color schemes defined earlier.

// Theme.kt
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext

@Composable
fun AppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    val context = LocalContext.current

    val colorScheme = when {
        dynamicColor &&
            Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
            darkTheme -> dynamicDarkColorScheme(context)

        dynamicColor &&
            Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ->
            dynamicLightColorScheme(context)

        darkTheme -> DarkColors

        else -> LightColors
    }

    MaterialTheme(
        colorScheme = colorScheme,
        content = content
    )
}

9.1 Explanation of the Code

  • darkTheme determines whether the application should use a dark appearance.
  • dynamicColor determines whether the app should attempt to use dynamic colors.
  • Build.VERSION.SDK_INT identifies the Android API level on the current device.
  • Build.VERSION_CODES.S represents Android 12, the minimum version for these dynamic color APIs.
  • LocalContext.current provides the Android context needed by the dynamic color functions.
  • dynamicDarkColorScheme(context) obtains the dynamic dark palette.
  • dynamicLightColorScheme(context) obtains the dynamic light palette.
  • DarkColors and LightColors provide fallbacks for cases where dynamic colors are not selected or available.
  • MaterialTheme makes the selected scheme available to the child composables.

This implementation follows the system theme by default. If the product needs a manual theme selector, the selected preference should be stored and passed into AppTheme instead of always relying on the system setting.

10. Using Theme Colors in UI Components

Once a theme is applied, UI components should use semantic color roles from MaterialTheme.colorScheme rather than hard-coded color values. This allows the same screen to adapt automatically to light, dark, and dynamic themes.

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun HomeScreen() {
    Column(
        modifier = Modifier.padding(16.dp)
    ) {
        Text(
            text = "Welcome to Android Learning",
            style = MaterialTheme.typography.headlineSmall,
            color = MaterialTheme.colorScheme.onBackground
        )

        Spacer(modifier = Modifier.height(16.dp))

        Card(
            modifier = Modifier.fillMaxWidth()
        ) {
            Column(
                modifier = Modifier.padding(16.dp)
            ) {
                Text(
                    text = "Explore Courses",
                    style = MaterialTheme.typography.titleMedium,
                    color = MaterialTheme.colorScheme.onSurface
                )

                Spacer(modifier = Modifier.height(8.dp))

                Button(onClick = { }) {
                    Text("Get Started")
                }
            }
        }
    }
}

Material 3 components such as cards and buttons use theme defaults when explicit colors are not supplied. For custom components, select compatible foreground and background roles to preserve readability and visual consistency.

11. Understanding Semantic Color Roles

Material 3 provides color roles that describe the purpose of a color. Using these roles helps developers maintain contrast and makes it easier to change themes without rewriting each screen.

Color Role Purpose Example
primary Main accent color. Primary buttons and active indicators.
onPrimary Content displayed on primary-colored elements. Text inside a primary button.
primaryContainer Container using a softer primary tone. Highlighted content area.
onPrimaryContainer Content placed on the primary container. Text within a highlighted area.
background Overall application background role. Screen background.
onBackground Content displayed on the background. Screen-level text.
surface General content surfaces. Cards, menus, and sheets.
onSurface Content displayed on surfaces. Card titles and body text.
error Error-related color. Validation feedback.

Use corresponding on... roles for text and icons placed on colored surfaces. For example, use onPrimary for content on a primary background and onSurface for content on a surface. Always verify contrast for the final palette.

12. Adding a Manual Theme Selector

Some applications let users choose between System Default, Light, and Dark modes. This gives users control over the application's appearance independently of the device's current setting.

A common approach is to define a theme preference with three possible values:

Preference Behavior
System Default Follow the device's current light or dark setting.
Light Always use the light color scheme.
Dark Always use the dark color scheme.

12.1 Define the Theme Preference

enum class ThemeMode {
    SYSTEM,
    LIGHT,
    DARK
}

12.2 Resolve the Preference

@Composable
fun ThemePreferenceExample(
    themeMode: ThemeMode,
    content: @Composable () -> Unit
) {
    val systemDark = isSystemInDarkTheme()

    val useDarkTheme = when (themeMode) {
        ThemeMode.SYSTEM -> systemDark
        ThemeMode.LIGHT -> false
        ThemeMode.DARK -> true
    }

    AppTheme(
        darkTheme = useDarkTheme,
        dynamicColor = true,
        content = content
    )
}

This example illustrates how to resolve a user preference. For a complete implementation, update AppTheme to accept the resolved values and ensure the selected theme preference is persisted through an appropriate settings mechanism, such as DataStore. The preference should be restored when the application starts.

13. Creating a Theme Settings Screen

A settings screen can present the three theme options in a radio group or another clear selection control. The selected option should be visually obvious and should update the application theme immediately.

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun ThemeSettingsScreen(
    selectedMode: ThemeMode,
    onModeSelected: (ThemeMode) -> Unit
) {
    Column(
        modifier = Modifier.padding(16.dp)
    ) {
        Text(
            text = "Appearance",
            style = MaterialTheme.typography.headlineSmall
        )

        ThemeMode.values().forEach { mode ->
            androidx.compose.foundation.layout.Row(
                verticalAlignment =
                    androidx.compose.ui.Alignment.CenterVertically
            ) {
                RadioButton(
                    selected = selectedMode == mode,
                    onClick = { onModeSelected(mode) }
                )

                Text(
                    text = when (mode) {
                        ThemeMode.SYSTEM -> "System Default"
                        ThemeMode.LIGHT -> "Light Mode"
                        ThemeMode.DARK -> "Dark Mode"
                    }
                )
            }
        }
    }
}

This is a simple UI example. Add the required imports and connect onModeSelected to state held by a ViewModel or settings layer. In Kotlin versions where enum entries are preferred, ThemeMode.entries can be used instead of ThemeMode.values().

14. Persisting the Theme Preference

If users choose a theme manually, their selection should normally remain active after the application closes and reopens. A persistent settings solution can save the selected theme mode.

Jetpack DataStore is a suitable option for storing small application preferences. A typical implementation would:

  1. Define a preference key for the selected theme mode.
  2. Read the saved preference as a Flow.
  3. Expose the value through a ViewModel or settings repository.
  4. Update the saved value when the user selects a different theme.
  5. Pass the resolved theme into the root theme composable.

A theme preference is not sensitive authentication data, but it should still be managed consistently. Avoid creating a new DataStore instance for every recomposition; follow the recommended application-level DataStore setup.

15. Best Practices for Dynamic Colors

  • Offer dynamic color when it fits the application's design goals.
  • Provide custom light and dark fallbacks.
  • Use semantic theme roles instead of hard-coded colors in reusable components.
  • Check that dynamic colors preserve adequate text contrast.
  • Test important UI states, including errors, disabled buttons, selected items, and dialogs.
  • Respect the user's selected theme preference.
  • Do not assume dynamic color is available on every Android device.
  • Keep brand identity and visual consistency in mind when deciding whether to use dynamic colors.

16. Accessibility Considerations

Supporting light and dark modes does not automatically make an application accessible. Each color scheme must be checked for contrast, readability, and clear visual states.

  • Ensure text remains readable on backgrounds and cards.
  • Use appropriate on... color roles for text and icons.
  • Do not use color alone to communicate an error or successful action.
  • Test buttons and fields in both themes.
  • Support large font settings without clipping text.
  • Provide accessible labels for controls that change theme settings.
  • Test the app with TalkBack and Android accessibility settings.
  • Check that custom images, charts, and illustrations also work in dark mode.

17. Common Problems and Troubleshooting

Problem Possible Cause Solution
Theme does not change with the system. The root composable does not use the current system theme. Use isSystemInDarkTheme() and apply the selected scheme through MaterialTheme.
Some text remains black in dark mode. The text color is hard-coded. Use semantic roles such as onSurface or onBackground.
Dynamic colors do not work on an older device. The Android version does not support the dynamic color APIs. Use custom light and dark color schemes as fallbacks.
Buttons have poor contrast. Foreground and background colors are incompatible. Use matching semantic roles and test the actual contrast.
User selection resets after restarting the app. The selected theme preference is not persisted. Store and restore the preference with a settings solution such as DataStore.
Some components do not follow the theme. Custom components use fixed colors or do not read theme values. Update custom components to use MaterialTheme.colorScheme.
Dark mode looks too bright. Light-theme surface colors are being reused in dark mode. Define a dedicated dark color scheme and review surfaces, borders, and accents.
Theme setting updates inconsistently. Theme state is not managed or passed consistently. Keep a single source of truth for the selected theme and observe it as state.

18. Testing Light, Dark, and Dynamic Themes

Testing ensures that the theme works on supported Android versions and that content remains readable in different configurations.

Testing Checklist

  • Run the application with system light mode enabled.
  • Run the application with system dark mode enabled.
  • Verify that all major screens use theme colors consistently.
  • Test dynamic colors on an Android 12 or later device or emulator.
  • Test fallback colors on an earlier Android version.
  • Change the wallpaper and verify dynamic color behavior where supported.
  • Test buttons, text fields, cards, dialogs, menus, and navigation components.
  • Check error messages and disabled states in both themes.
  • Verify that manual theme preferences survive an application restart if persistence is implemented.
  • Check contrast, text scaling, and accessibility behavior.

19. Practical Project: Theme-Aware Learning App

Project Goal: Build an Android learning application that supports light mode, dark mode, and dynamic colors using Jetpack Compose and Material 3.

Project Requirements

  1. Create a custom light color scheme.
  2. Create a custom dark color scheme.
  3. Build a root theme composable that selects the correct scheme.
  4. Add dynamic color support for Android 12 and later.
  5. Use fallback colors on older Android versions.
  6. Create a home screen with cards, text, and buttons.
  7. Use semantic color roles in all reusable UI components.
  8. Add a settings screen for System Default, Light, and Dark modes.
  9. Persist the user's theme selection.
  10. Test the interface in multiple modes and on different screen sizes.

Expected Outcome

The completed application should adapt to system settings or the user's chosen preference, use dynamic colors where enabled and supported, and maintain consistent readable styling across its screens.

20. Interview Questions and Answers

Q1. What is light mode in Android?

Light mode is a visual theme that generally uses light backgrounds, darker text, and coordinated accent colors.

Q2. What is dark mode?

Dark mode uses dark backgrounds and surfaces with lighter foreground content to provide an alternative appearance.

Q3. What are dynamic colors?

Dynamic colors allow an application to derive a coordinated theme from the user's wallpaper on supported Android versions.

Q4. Which Android version supports Material 3 dynamic colors?

The dynamic color APIs discussed here are available on Android 12 and later, corresponding to API level 31 and above.

Q5. What is isSystemInDarkTheme()?

It is a Compose API that indicates whether the system currently uses a dark theme.

Q6. What is the difference between lightColorScheme() and darkColorScheme()?

They create Material 3 color schemes intended for light and dark appearances respectively.

Q7. Why should developers use MaterialTheme.colorScheme?

It provides semantic theme colors to UI components, making it easier to keep the application consistent across different themes.

Q8. Why is a fallback theme necessary?

Dynamic colors are not available through these APIs on all Android versions. Custom color schemes ensure that the application still has a defined appearance.

Q9. How can users choose their own theme?

An application can provide System Default, Light, and Dark options and pass the selected preference to the root theme composable.

Q10. How can the theme selection be remembered?

Persist the preference using a settings mechanism such as DataStore, then restore it when the application starts.

Q11. Why should hard-coded colors be avoided?

Hard-coded colors can prevent components from adapting to the active theme and may create readability problems in dark mode.

Q12. Does dark mode always save battery?

No. The effect depends on the display technology, brightness, and screen content. Dark interfaces can reduce power consumption on OLED displays in suitable conditions.

21. Quick Revision

  • Light mode generally uses light backgrounds and dark foreground content.
  • Dark mode uses dark surfaces and lighter foreground content.
  • Dynamic colors can personalize the UI using colors derived from the user's wallpaper.
  • Material 3 provides light, dark, and dynamic color scheme APIs.
  • isSystemInDarkTheme() detects the current system theme.
  • Dynamic color APIs require Android 12 or later.
  • Custom color schemes should be available as fallbacks.
  • MaterialTheme.colorScheme helps components adapt to the active theme.
  • DataStore can persist a user's manual theme preference.
  • Always test contrast, component states, and accessibility in every supported theme.

22. Conclusion

Applying light mode, dark mode, and dynamic colors helps Android developers create flexible, personalized, and consistent user interfaces. Jetpack Compose and Material Design 3 simplify theme management through reusable color schemes and a centralized MaterialTheme.

For a reliable implementation, detect the system theme or respect the user's saved preference, provide custom fallback schemes, use semantic color roles, and test the application across different Android versions. A thoughtful theme implementation improves usability, maintainability, and visual consistency throughout the application.


23. Additional Learning Resources

Previous Next
whatsapp