Color Systems for Mobile Apps: Roles, Themes and States
How to build a mobile app color system with semantic roles, light and dark themes, pressed, disabled and focus states, and contrast that holds up outdoors.
By ColorOverflow Editorial TeamUpdated November 13, 20267 min readIntermediate
A mobile app can't get by with a list of five brand colors. It has light and dark themes, buttons that are pressed or disabled, errors, sheets that slide over other content, and screens used outdoors in bright light. The way apps handle all this is a color system: a small set of named roles, each with a value for every theme.
This guide explains the roles most app design systems share, how to give each one a light and a dark value, how to handle states, and how to keep everything readable on a small screen in sunlight.
Why roles instead of colors
In a color system, a screen never asks for indigo or gray 900. It asks for the primary color, the surface color or the text color that sits on a surface. The theme decides which actual color that is. Switch the theme and every screen updates, because no screen ever named a specific color.
Roles also make contrast predictable. If every role comes with a matching role for content placed on top of it, and you check each pair once, any screen built from those pairs is readable by design.
The common semantic roles
Different design systems use different names, but they cover the same ground. A widespread convention is to pair each fill role with an on role, meaning the color for text and icons placed on that fill.
- Primary and on-primary: the main brand color for key buttons, active tabs and selected controls, plus the color of labels on it.
- Primary container and on-primary container: a softer, low-emphasis version of the primary for chips, selected list rows and less important buttons.
- Secondary: a supporting color for filters, toggles and less prominent accents. Many apps can do without it.
- Background: the base of the screen behind everything.
- Surface and on-surface: cards, sheets, dialogs and bars, plus the main text and icon color on them.
- Raised surface: one or more steps for surfaces that sit above others, such as a sheet over a list.
- On-surface variant: muted text and icons for secondary information such as timestamps and hints.
- Outline: borders of inputs and cards, and dividers, with a stronger and a subtler version if needed.
- Error and on-error: destructive actions and validation messages. Some systems add warning, success and info in the same pattern.
An example app palette, light theme
- #4F46E5
- #FFFFFF
- #E0E7FF
- #312E81
- #F9FAFB
- #111827
- #4B5563
- #6B7280
- #B91C1C
Primary, on-primary and surface, primary container, on-primary container, background, on-surface, on-surface variant, outline and error.
- On-primary white on primary #4F46E5: 6.28:1.
- On-primary container #312E81 on container #E0E7FF: 9.27:1.
- On-surface #111827 on a white surface: 17.73:1, and 16.97:1 on the #F9FAFB background.
- On-surface variant #4B5563: 7.55:1 on white and 7.23:1 on the background, so muted text stays strong.
- Outline #6B7280: 4.83:1 on white, above the 3:1 needed for input borders.
- Error #B91C1C: 6.47:1 on white as text, and white labels on it also reach 6.47:1.
The same roles in a dark theme
A dark theme is not the light theme inverted. Surfaces become very dark grays, and many apps avoid pure black so that raised surfaces can be shown as slightly lighter steps. Saturated brand colors usually need lighter, softer versions, because a strong indigo that works on white looks heavy and loses contrast on near-black.
- #A5B4FC
- #1E1B4B
- #3730A3
- #E0E7FF
- #111827
- #1F2937
- #F3F4F6
- #D1D5DB
- #9CA3AF
- #FCA5A5
- #450A0A
Primary, on-primary, container, on-container, surface, raised surface, on-surface, on-surface variant, outline, error and on-error.
- Primary #A5B4FC with dark on-primary #1E1B4B: 8.01:1. The primary itself reaches 8.89:1 on the surface, so it also works for links and active icons.
- On-container #E0E7FF on container #3730A3: 8.06:1.
- On-surface #F3F4F6: 16.11:1 on the surface #111827 and 13.33:1 on the raised surface #1F2937.
- On-surface variant #D1D5DB: 12.03:1 on the surface.
- Outline #9CA3AF: 6.98:1 on the surface and 5.78:1 on the raised surface.
- Error #FCA5A5: 9.34:1 on the surface, with on-error #450A0A on it at 8.50:1.
One detail to watch: the container #3730A3 is only 1.78:1 against the dark surface. That's fine, because its label carries the contrast, but if the container is the only sign that a row is selected, add a check mark or an outline too.
JSON
{
"primary": { "light": "#4f46e5", "dark": "#a5b4fc" },
"on-primary": { "light": "#ffffff", "dark": "#1e1b4b" },
"surface": { "light": "#ffffff", "dark": "#111827" },
"on-surface": { "light": "#111827", "dark": "#f3f4f6" },
"on-surface-variant": { "light": "#4b5563", "dark": "#d1d5db" },
"outline": { "light": "#6b7280", "dark": "#9ca3af" },
"error": { "light": "#b91c1c", "dark": "#fca5a5" },
"on-error": { "light": "#ffffff", "dark": "#450a0a" }
}How you store and name these values, and how they reach code on each platform, is the subject of our guide Design Tokens and Color Systems.
Read the full guideDesign Tokens and Color Systems: From Palette to CodeBuilding the scales behind the roles
The role values don't have to be invented one by one. Most systems start from a tonal scale for each key color, running from very light to very dark, and pick role values from it: a mid step for the light-theme primary, a pale step for its container, a light step for the dark-theme primary, and so on. The Shades and Tints tool builds such a scale from a single color, from 950 to 50.
Shades & TintsTurn one color into a full scale of lighter and darker versions.States: pressed, disabled and focus
Interactive elements change color as people use them. Define these changes once in the system, not separately for every button.
- Pressed and hovered: a common approach is to lay a thin, semi-transparent layer of the on color over the fill. It works for every role and both themes without adding new colors. Make sure the label still passes contrast on the pressed color.
- Selected: use the container color, plus a non-color sign such as a check mark, bold text or an indicator bar.
- Disabled: lower the emphasis, typically by reducing opacity or switching to a neutral, so the control is clearly unavailable but still recognizable. WCAG doesn't require disabled controls to meet contrast, but they shouldn't vanish.
- Focus: a visible ring or outline for keyboard, switch and other assistive input. It needs at least 3:1 against what's around it. In the example, the light primary on white gives 6.28:1 and the dark primary on the dark surface gives 8.89:1.
- Error state on inputs: change the outline to the error color and add an error message and icon, not just a red border.
Small screens and bright sunlight
Phones are used in places a desk monitor never goes: on a train, at a bus stop, on a beach. Glare from bright light washes out the screen, and pairs that passed comfortably indoors become hard to read. Small text and thin icons suffer first.
- Treat 4.5:1 as a minimum, not a target. For primary text and key actions, aim for 7:1, the AAA level, when you can. Every text pair in the example palettes clears 7:1 except the light theme's primary and error pairs, at 6.28:1 and 6.47:1.
- Keep muted text muted by size or weight rather than by making it very light gray.
- Avoid thin, light-colored icons as the only label for an action.
- Use outlines that pass 3:1, because subtle borders are the first thing glare erases.
- Test on a real phone outdoors, at medium brightness, not only in a design tool.
Preview your palette on an app
The Palette Visualizer includes a mobile app screen. Load your palette and check whether the primary reads as the main action, whether cards separate from the background, and whether the muted text is still easy to read at phone size.
Palette VisualizerSee your palette on real designs, outfits and rooms.Common mistakes
- Naming colors by hue in code, so a rebrand means renaming everything.
- Making the dark theme by inverting the light one, which leaves brand colors too saturated and too dark.
- Defining fills without the matching on colors, so text color is chosen screen by screen.
- Showing a selected or error state with color alone.
- Disabled controls so faint they look like empty space.
- Only checking contrast indoors on a large monitor.
Quick checklist
- Every color in the app comes from a named role.
- Every fill role has an on role, and each pair is checked in light and dark.
- Text pairs reach at least 4.5:1, ideally 7:1 for primary text.
- Outlines, focus rings and meaningful icons reach 3:1.
- Pressed, selected, disabled and focus states are defined once for the whole system.
- Selected and error states have a non-color signal.
