Product colors can be defined physically or logically. Physical colors can be specific RGB values (e.g. "#202124"), Material values (e.g. "Google Grey 900"), or occasionally referential values (“10% white atop toolbar background”). Logical colors are defined functionally, and may be more or less specific depending on context (e.g. “body text”, “call to action”, “hovered button background”, or “profile menu bottom stroke”).
Distinguishing logical and physical use in code makes code amenable to systematic changes (Material Design, Harmony, MD Refresh, dark mode...). It increases behavioral consistency and improves readability. It usually results in greater UI accessibility. Finally, it helps avoid edge case bugs that are easy to miss, such as problems with custom themes or the GTK native appearance.
The following best practices focus on distinguishing physical and logical color use, along with some rules for consistent code organization.
[TOC]
UX and engineering should agree on color function (logical colors) rather than just presentation (physical colors). A mock with only physical values isn't amenable to the separation described above; there should be a rationale that describes how the various colors relate to each other and to the relevant specs (e.g. Desktop Design System Guidelines (Internal Link)). Ideally, physical color values aren't even necessary, because all values refer to pre-specified concepts; if new colors are needed, UX leadership and the toolkit team should agree on how to incorporate them into an updated system.
Use logical color values in a View
. This means avoiding
SkColorSet[A]RGB(...)
, SK_ColorBLACK
, gfx::kGoogleGrey900
, and the like.
Instead, obtain colors by requesting them (by identifier) from an appropriate
theming object. View
s in ui/
should call
GetColorProvider()->GetColor(id)
; View
s in chrome/
should call that or
GetThemeProvider()->GetColor(id)
.
|||---|||
Avoid
Old code in tooltip_icon.cc
used hardcoded colors:
Best practice
The current version uses color IDs, allowing the icon colors to potentially be tied to other icon colors:
|||---|||
|||---|||
void TooltipIcon::SetDrawAsHovered(bool hovered) {
SetImage(gfx::CreateVectorIcon(
vector_icons::kInfoOutlineIcon,
tooltip_icon_size_,
hovered
? SkColorSetARGB(
0xBD, 0, 0, 0)
: SkColorSetARGB(
0xBD, 0x44, 0x44, 0x44)));
}
void TooltipIcon::SetDrawAsHovered(bool hovered) {
SetImage(gfx::CreateVectorIcon(
vector_icons::kInfoOutlineIcon,
tooltip_icon_size_,
GetColorProvider()->GetColor(
hovered
? ui::kColorHelpIconActive
: ui::kColorHelpIconInactive)));
}
|||---|||
Set colors in an OnThemeChanged()
override; update elsewhere as needed.
Theming objects are generally provided through the Widget
, and so will be null
in a View
's constructor. This isn't a problem because View
s are not yet
visible at construction. When a View
is first placed in a Widget
hierarchy,
and any time afterward that the theme may have changed, it will receive a call
to OnThemeChanged()
. Thus in the majority of cases, setting colors in
OnThemeChanged()
handles both initial presentation and theme changes
correctly. In cases where the color to use depends on state, both
OnThemeChanged()
and the state change handlers may need to call common code
that sets the correct colors.
|||---|||
Avoid
Old code in file_system_access_usage_bubble_view.cc
set colors in its
constructor since it seemed reasonable to do:
Best practice
The current version
moves this to OnThemeChanged()
and thus handles theme changes correctly:
|||---|||
|||---|||
explicit CollapsibleListView(ui::TableModel* model) {
...
auto button =
views::CreateVectorToggleImageButton(this);
...
const SkColor icon_color =
ui::NativeTheme::GetInstanceForNativeUi()->
GetSystemColor(
ui::NativeTheme::
kColorId_DefaultIconColor);
views::SetImageFromVectorIconWithColor(
button.get(), kCaretDownIcon,
ui::TableModel::kIconSize, icon_color);
views::SetToggledImageFromVectorIconWithColor(
button.get(), kCaretUpIcon,
ui::TableModel::kIconSize, icon_color);
...
}
explicit CollapsibleListView(ui::TableModel* model) {
...
auto button =
views::CreateVectorToggleImageButton(this);
...
expand_collapse_button_ =
label_container->AddChildView(std::move(button));
...
}
void CollapsibleListView::OnThemeChanged() {
views::View::OnThemeChanged();
const auto* color_provider = GetColorProvider();
const SkColor icon_color =
color_provider->GetColor(ui::kColorIcon);
const SkColor disabled_icon_color =
color_provider->GetColor(ui::kColorIconDisabled);
views::SetImageFromVectorIconWithColor(
expand_collapse_button_,
vector_icons::kCaretDownIcon,
ui::TableModel::kIconSize, icon_color,
disabled_icon_color);
views::SetToggledImageFromVectorIconWithColor(
expand_collapse_button_,
vector_icons::kCaretUpIcon,
ui::TableModel::kIconSize, icon_color,
disabled_icon_color);
}
|||---|||
Keep color use local. View
s should generally access color IDs only for
themselves, and not get or set colors on other View
s (e.g. children). Because
the order of OnThemeChanged()
calls is not guaranteed, getting a cached color
from another View
may return an incorrect value; and setting physical
properties of other View
s reduces encapsulation. Instead, convey logical state
changes as necessary, let each View
manage its own colors, and use file-scope
View
subclasses freely to define the behavior of specific View
s.
|||---|||
Avoid
Old code in custom_tab_bar_view.cc
tried to keep a parent's color in sync with
its child:
Best practice
Current version queries a color API that is guaranteed to have an up-to-date value:
|||---|||
|||---|||
class CustomTabBarTitleOriginView : public views::View {
public:
...
SkColor GetLocationColor() const {
return location_label_->GetEnabledColor();
}
...
private:
views::Label* location_label_;
};
class CustomTabBarTitleOriginView : public views::View {
public:
...
SkColor GetLocationColor() const {
return views::style::GetColor(
*this, CONTEXT_DIALOG_BODY_TEXT_SMALL,
views::style::TextStyle::STYLE_PRIMARY);
}
...
};
|||---|||
Where color information must be passed on, use color IDs, not SkColor
or
ImageSkia
. For example, menu items with images should use
ImageModel::FromVectorIcon(
..., id)
, as the other ImageModel
factory
functions require a physical color at some prior stage, and make handling theme
changes difficult.
|||---|||
Old code in recovery_install_global_error.cc
embedded colors into the icon
stored in the menu model:
Current version
uses an ImageModel
to convey just the logical color, allowing consumers to
apply physical colors:
|||---|||
|||---|||
Avoid
gfx::Image
RecoveryInstallGlobalError::MenuItemIcon() {
return gfx::Image(gfx::CreateVectorIcon(
kBrowserToolsUpdateIcon,
ui::NativeTheme::GetInstanceForNativeUi()->
GetSystemColor(
ui::NativeTheme::
kColorId_AlertSeverityHigh)));
}
Best practice
ui::ImageModel
RecoveryInstallGlobalError::MenuItemIcon() {
return ui::ImageModel::FromVectorIcon(
kBrowserToolsUpdateIcon,
ui::kColorAlertHighSeverity);
}
|||---|||
For new UI, add new, specific color identifiers. If a mock for a new
FrobberMenu
UI calls for the background color to be the same as the
FooBarMenu
background, don't reuse kFooBarMenuBackgroundColor
when
implementing the new menu, as the name will no longer be accurate. Add a
kFrobberMenuBackgroundColor
identifier and use that instead. ui/
code should
add identifiers to the COLOR_IDS
macro; chrome/
code should typically add to the
NotOverwritableByUserThemeProperty
enum.
|||---|||
Avoid
Old code in menu_controller_scroll_view_container.cc
reused a text color
constant to paint the drop indicator, since they have the same physical color:
Best practice
Current version defines these as distinct logical colors with the same default physical color, better accommodating platforms where menu text and menu icons may differ:
|||---|||
|||---|||
void SubmenuView::PaintChildren(
const PaintInfo& paint_info) {
...
const SkColor drop_indicator_color =
GetNativeTheme()->GetSystemColor(
ui::NativeTheme::
kColorId_HighlightedMenuItemForegroundColor);
recorder.canvas()->FillRect(
bounds, drop_indicator_color);
}
SkColor GetAuraColor(
NativeTheme::ColorId color_id,
const NativeTheme* base_theme,
NativeTheme::ColorScheme color_scheme) {
switch (color_id) {
...
case NativeTheme::
kColorId_HighlightedMenuItemForegroundColor:
case NativeTheme::kColorId_MenuDropIndicator:
return gfx::kGoogleGrey200;
...
}
}
void SubmenuView::PaintChildren(
const PaintInfo& paint_info) {
...
const SkColor drop_indicator_color =
GetColorProvider()->GetColor(
ui::kColorMenuDropmarker);
recorder.canvas()->FillRect(
bounds, drop_indicator_color);
...
}
|||---|||
Define identifiers' physical values by their relationships to other colors.
In most cases, new UI elements are using the same underlying concepts as other
UI elements. So instead of kFrobberMenuBackgroundColor
copying the definition
of kFooBarMenuBackgroundColor
, both menus' colors should probably be defined
in terms of some more generic underlying concept (e.g. a common toolkit-level
menu or background color). Even in more one-off cases, colors can still be
relational; black text on a white background is not simply black in the
abstract, but black because it contrasts with the background. UI colors can
usually similarly be defined as contrasting or blending against other colors,
using the functions in color_utils.h
.
In most cases, a correct relational definition for a color will work well in
both light and dark mode, and custom themes as well.
|||---|||
Avoid
Old code in common_theme.cc
defines a disabled button color absolutely:
Best practice
Current version makes it clear that the disabled color is related to the normal color:
|||---|||
|||---|||
SkColor GetAuraColor(
NativeTheme::ColorId color_id,
const NativeTheme* base_theme,
NativeTheme::ColorScheme color_scheme) {
...
switch (color_id) {
case NativeTheme::
kColorId_ProminentButtonDisabledColor:
return gfx::kGoogleGrey100;
...
}
}
SkColor GetAuraColor(
NativeTheme::ColorId color_id,
const NativeTheme* base_theme,
NativeTheme::ColorScheme color_scheme) {
...
switch (color_id) {
case NativeTheme::
kColorId_ProminentButtonDisabledColor: {
const SkColor bg = base_theme->GetSystemColor(
NativeTheme::kColorId_ButtonColor,
color_scheme);
return color_utils::BlendForMinContrast(
bg, bg, std::nullopt, 1.2f).color;
}
...
}
}
|||---|||
Use image resources that are theme-neutral. Most vector images should not encode color information at all; in some cases, badging can be used to break a vector into a common core and a set of recolorable overlays. Full-color raster images that work on any background color can be used, but images that assume a particular background color, or that are broken into "light" and "dark" variants, should not be used; these are problematic both today (with custom themes and GTK) and in the future (if we modify the default light/dark theme colors or Material Design palettes). These cases are tricky to address and generally require cooperation between the visual designer, the toolkit team, and the UI engineer.
Current code uses two full color raster images, one that assumes a light background and the other that assumes a dark background:
|||---|||
|||---|||
An improved version could split the image into a fixed color portion and an uncolored portion using alpha that is programmatically computed based on the desired foreground color.
Do not use colors outside a known rooted entity, since doing so correctly is
difficult. Global access to theming objects is deprecated and will eventually be
removed, since it's error-prone and hinders future design goals. Direct use of
physical colors and obtaining colors from View
instances are problematic for
reasons given above. If you need something like this, talk to the toolkit team
about the best approach.
|||---|||
Avoid
Old code in tab_strip_ui_handler.cc
uses global theming objects:
Best practice
Current version
gets colors from its embedding View
; this will still require the caller to
guarantee the View
's colors are up to date, monitor for potential changes in
those colors, etc. The View
should be rooted within a Widget
:
|||---|||
|||---|||
void TabStripUIHandler::HandleGetThemeColors(
const base::ListValue* args) {
...
const ui::ThemeProvider& tp =
ThemeService::GetThemeProviderForProfile(
browser_->profile());
base::DictionaryValue colors;
colors.SetString(
"--tabstrip-background-color",
color_utils::SkColorToRgbaString(
tp.GetColor(
ThemeProperties::COLOR_FRAME)));
...
}
class TabStripPageHandler : public tab_strip::mojom::PageHandler,
public TabStripModelObserver,
public content::WebContentsDelegate,
public ThemeServiceObserver,
public ui::NativeThemeObserver {
...
private:
const raw_ptr<TabStripUIEmbedder> embedder_;
...
}
void TabStripPageHandler::GetThemeColors(
GetThemeColorsCallback callback) {
// This should return an object of CSS variables to rgba values so that
// the WebUI can use the CSS variables to color the tab strip
base::flat_map<std::string, std::string> colors;
colors["--tabstrip-background-color"] = color_utils::SkColorToRgbaString(
embedder_->GetColor(ThemeProperties::COLOR_FRAME_ACTIVE));
colors["--tabstrip-tab-background-color"] = color_utils::SkColorToRgbaString(
embedder_->GetColor(ThemeProperties::COLOR_TOOLBAR));
...
}
|||---|||
Under MacOS, a top-level window (Widget
) may not be available in the process
from which the correct ThemeProvider
or ColorProvider
are obtained. In this
case, the AppController
is available from which the ThemeProvider
can be
obtained.
NOTE: For code running within a top-level window, refer the previous best practice section for the preferred technique.
@interface AppController
: NSObject <NSUserInterfaceValidations,
NSMenuDelegate,
NSApplicationDelegate,
ASWebAuthenticationSessionWebBrowserSessionHandling> {
...
// Returns the last active ThemeProvider. It is only valid to call this with a
// last available profile.
- (const ui::ThemeProvider&)lastActiveThemeProvider;
...
@end
class HistoryMenuBridge : public sessions::TabRestoreServiceObserver,
public MainMenuItem,
public history::HistoryServiceObserver {
public:
...
// Adds an item for the group entry with a submenu containing its tabs.
// Returns whether the item was successfully added.
bool AddGroupEntryToMenu(sessions::tab_restore::Group* group,
NSMenu* menu,
NSInteger tag,
NSInteger index);
...
bool HistoryMenuBridge::AddGroupEntryToMenu(
sessions::tab_restore::Group* group,
NSMenu* menu,
NSInteger tag,
NSInteger index) {
...
// Set the icon of the group to the group color circle.
const auto& theme = [AppController.sharedController lastActiveThemeProvider];
const int color_id =
GetTabGroupContextMenuColorIdDeprecated(group->visual_data.color());
gfx::ImageSkia group_icon = gfx::CreateVectorIcon(
kTabGroupIcon, gfx::kFaviconSize, theme.GetColor(color_id));
|||---|||
For WebUI, content::WebContents
is used for obtaining the ThemeProvider
.
namespace webui {
...
#if defined(TOOLKIT_VIEWS)
...
// Returns the ThemeProvider instance associated with the given web contents.
const ui::ThemeProvider* GetThemeProvider(content::WebContents* web_contents);
...
#endif // defined(TOOLKIT_VIEWS)
} // namespace webui
void NTPResourceCache::CreateNewTabIncognitoCSS(
const content::WebContents::Getter& wc_getter) {
auto* web_contents = wc_getter.Run();
const ui::NativeTheme* native_theme = webui::GetNativeThemeDeprecated(web_contents);
DCHECK(native_theme);
// Requesting the incognito CSS is only done from within incognito browser
// windows. The ThemeProvider associated with the requesting WebContents will
// wrap the relevant incognito bits.
const ui::ThemeProvider* tp = webui::GetThemeProviderDeprecated(web_contents);
DCHECK(tp);
...
}