Photo by Alexander Ant on Unsplash

How to create a semantic color system on Android

Posted: 01 Jun 2022. Last modified on 04-Jun-22.

This article will take about 7 minutes to read.


Semantic colors provide a way to organize a color system, so that any color references are given by intention instead of by value.

But when I say semantic, what am I talking about? According to wordnik, one definition of semantic is:

Reflecting intended structure and meaning.

A simple example of applying semantics to colors would be naming a color background instead of white. By naming the color according to where it should be used, instead of what it literally is, we can greatly simplify how colors are used throughout large projects.

How are Semantic Colors Used Across the Industry?

Semantic colors were popularized on Android with the 2014 introduction of Google’s Material color palettes.

The Material library broke color resolution down into 3 separate layers:

Each layer is able to point to a resource in the layer below.

Why would we need to use them?

This layered approach to color resolution creates the opportunity for us to define themes that are sometimes called “color modes”. You will probably already be familiar with Dark Mode. Other color modes are possible, too, such as:

All of these color modes convey something important to the user. It could be relevant accessibility information, or the state that their account is in.

Architecturally though, how should we handle all of these different color mappings? A naive approach might involve doing something like the following:

fun bindTextView(user: User){
    binding.myTextView?.apply{
        backgroundColor = when {
            user.isInDarkMode -> getColor(R.color.grey)
            user.isAdmin -> getColor(R.color.magenta)
            else -> getColor(R.color.teal)
        }
    }
}

While this might work in very small codebases, it’s not a scalable solution. We wouldn’t want to have to duplicate this logic every time we want to use these colors.

A more scalable solution would be to call the color by its theme name, and change what that theme name pointing to.

override fun onCreate() {
    setTheme(Themes.Admin.resid)
}

fun bindTextView(user: User){
    binding.myTextView?.apply{
        backgroundColor = ContextCompat.getColor(context, R.attr.primary)
    }
}

Using a system like this, the color that we use is themeable, and the mapping happens for us automatically. If we needed to switch out what a color stood for in dark mode, this new way would make that pretty easy. A color like background could be updated to point to grey900 while in dark mode, instead of white in default mode.

How does branding encourage semantic colors?

Brands like to differntiate! Over time, simple words like primary and secondary start to get used for a wide range of use cases. If we want to change the UI for one of those use cases later, more specific names for the color need to be invented.

Eventually, a new use case will emerge that needs to be styled differently per theme. It might be that only one of your themes needs to differentiate - it could look fine on the rest.

It’s easy to imagine a generic color like background being split into backgroundButton and backgroundCard, because they are used differently in different places. You may not want to have all of your buttons change color, just because your cards have!

Accessibility

Sometimes these changes are required because of accessibility concerns, as well.

One example of this would be one which changes the primary color of the app to black. This is a perfectly valid design decision - sometimes black is what you want!

However, you’ll have to be able to adapt to the fact that your inline links will be the same color as your text content. How will your users know that they can click on that link? Usually, clickable text is colored and underlined, to give users a hint that a certain span of text is interactable.

An example of how changing themes can cause accessibility concerns, if the tokens are not granular enough

One way to fix this issue is to create an action color, separate from your primary color. That will give you the freedom to change just the color of the links, while keeping the rest of the primary-colored content untouched.

As you find new use cases throughout your app, you might get into greater degrees of specialization. It should be common to see things like PrimaryActive, InteractiveMuted, and SuccessLabel in your app. Each new color allows for greater brand expression across the family of apps you support.

How are semantic colors structured in code?

You should have a couple of layers of color tokens, to enable theme creation.

The first layer, which you should be referencing exclusively across the app, will be the theme layer. The resources should look like ?attr/color_name. Each semantic color is just a pointer to an AttrRes XML resource of the same name.

The second layer is for creating color scales. The resources should look like @color/color_name, and they should point to hex colors that look like #ffffff.

To set a new theme on the app, we just need to go to the hosting Activity and call Activity.setTheme(style) with a <style> that contains the full list of AttrRes values we want to use.

Why do we need to follow this layering approach?

To understand what’s happening under the hood, we need to understand the difference between AttrRes and ColorRes values.

ColorRes values are compiled into the app, so they are fixed at compile time. Once the R file is generated, the ColorRes values can never be changed again.

This permanence can be an issue if we want to support something like paid user or dark mode: If we coded the whole app in ColorRes values, we’d be stuck either shipping a separate dark mode app to the Play Store, or littering our codebase with conditionals that call for one color or another based on the user’s preference. It’s easy to forget to put in these checks, resulting in a UI that only partially changes when a new theme is applied.

AttrRes values solve this issue. During compilation, they just reserve a space in the R file, much like declaring a variable. During runtime, this space can be reassigned with whatever design tokens we want.

Conclusion

Semantic colors are a keystone of modern design systems that need to be able to apply multiple palettes to an app during runtime. The Android implementation allows us to pull from varied color sources and deliver colors that will remain consistent throughout the app while also allowing for brand and experience differentiation. If you have follow-up questions about this topic or about implementation specifics, please reach out!