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.
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:
primary
red100
#ff0000
Each layer is able to point to a resource in the layer below.
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.
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!
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.
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.
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.
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.
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!