Appearance
Forced Colours
Why?
Respect the users needs, applying semantically meaningful system colours.
Many Design Systems have a 'High Contrast' mode, with a more limited set of colours in a significantly higher contrast ratio, often implemented as a simple theme that can be swapped in to replace the standard theme.
Whilst a laudable effort, this cannot be guaranteed to satisfy all users. There are many reasons that end users will require a different set of colours for your UI, and the one you provide will not be able to handle all of those cases.
Happily, all operating systems come with a custom 'high contrast' (or similarly named) mode, where users can specify a limited set of colours they wish to see across applications. This is communicated to the browser through the forced-colors media feature, with values provided through the <system-color> data type.
Respecting the user's forced colour preferences greatly increases the chances of serving a usable UI.
Palette
There are three principle surfaces defined for system colours; Canvas, ButtonFace and Input. Each has a corresponding foreground colour, and can be used to map to non-interactive containers, interactive controls and inputs, respectively.
Container Tokens
Name
Card
Primary Control
Sentiment Control
Container
Standard
Active
Background
Layer/Background/PanelLayer/Background/PrimaryLayer/Background/Sentiment Foreground
Layer/Foreground/PanelLayer/Foreground/PrimaryLayer/Foreground/Sentiment Border
Layer/Border/PanelLayer/Border/PrimaryLayer/Border/SentimentContainer
Standard
Disabled
Background
TransparentSemantics/Disabled/BackgroundSemantics/Disabled/Background Foreground
Semantics/Disabled/ForegroundSemantics/Disabled/ForegroundSemantics/Disabled/Foreground Border
Semantics/Disabled/BorderSemantics/Disabled/BorderSemantics/Disabled/BorderContainer
Forced Colours
Active
Background
System/CanvasSystem/ButtonFaceSystem/ButtonFace Foreground
System/CanvasTextSystem/ButtonTextSystem/ButtonText Border
System/CanvasTextSystem/ButtonBorderSystem/ButtonBorderContainer
Forced Colours
Disabled
Background
TransparentTransparentTransparent Foreground
System/GrayTextSystem/GrayTextSystem/GrayText Border
System/GrayTextSystem/GrayTextSystem/GrayTextWe then have a set of variables to switch between standard and forced colours mode:
Forced Colours
Name
Standard
Forced Colours
Container
Active
Background
Container/Standard/Active/BackgroundContainer/Forced Colours/Active/Background Foreground
Container/Standard/Active/ForegroundContainer/Forced Colours/Active/Foreground Border
Container/Standard/Active/BorderContainer/Forced Colours/Active/BorderContainer
Disabled
Background
Container/Standard/Disabled/BackgroundContainer/Forced Colours/Disabled/Background Foreground
Container/Standard/Disabled/ForegroundContainer/Forced Colours/Disabled/Foreground Border
Container/Standard/Disabled/BorderContainer/Forced Colours/Disabled/BorderBefore our final layer that selects between Active/Disabled states:
Container State
Name
Active
Disabled
Container
Background
Container/Active/BackgroundContainer/Disabled/Background Foreground
Container/Active/ForegroundContainer/Disabled/Foreground Border
Container/Active/BorderContainer/Disabled/BorderActive
Forced Colours
Forced Colours (disabled)
scss
@use "layer";
@use "disabled";
@use "forced-colours";
.container-sentiment-control {
@include layer.sentiment-background;
@include layer.sentiment-foreground;
@include layer.sentiment-border;
@include disabled.foreground;
@include disabled.background;
@include disabled.border;
@include forced-colours.forced-colours-button;
}scss
@mixin forced-colours-button {
@media (forced-colors: active) {
forced-color-adjust: none;
background-color: ButtonFace;
border-color: ButtonBorder;
color: ButtonText;
&:disabled,
&[aria-disabled="true"] {
background-color: transparent;
border-color: GrayText;
color: GrayText;
}
}
}
@mixin forced-colours-panel {
@media (forced-colors: active) {
forced-color-adjust: none;
background-color: Canvas;
border-color: CanvasText;
color: CanvasText;
&:disabled,
&[aria-disabled="true"] {
background-color: transparent;
border-color: GrayText;
color: GrayText;
}
}
}scss
@mixin foreground {
&:disabled,
&[aria-disabled="true"] {
background-color: var(--disabled-foreground);
}
}
@mixin background {
&:disabled,
&[aria-disabled="true"] {
background-color: var(--disabled-background);
}
}
@mixin border {
&:disabled,
&[aria-disabled="true"] {
background-color: var(--disabled-border);
}
}scss
@mixin sentiment-background {
background-color: var(--sentiment-background);
}
@mixin sentiment-foreground {
color: var(--sentiment-foreground);
}
@mixin sentiment-border {
border-color: var(--sentiment-border);
}scss
@use "behavioural" as *;
@each $sentiment in (positive, negative) {
.sentiment-#{$sentiment} {
@include behavioural(
--sentiment-background,
"--sentiment-#{$sentiment}-background"
);
@include behavioural(
--sentiment-foreground,
"--sentiment-#{$sentiment}-foreground"
);
@include behavioural(
--sentiment-border,
"--sentiment-#{$sentiment}-border"
);
}
}scss
@mixin behavioural($prop, $color) {
#{$prop}: var(#{$color}-default);
&:hover {
#{$prop}: var(#{$color}-hover);
}
&:active {
#{$prop}: var(#{$color}-active);
}
}html
<div class="container-sentiment-control sentiment-positive">Active</div>
<div
class="container-sentiment-control sentiment-positive"
aria-disabled="true"
>
Disabled
</div>Selected Items
It may be the case that the standard appearance of a 'selected' item (a toggle button, for instance) is equivalent to the appearance of a primary button.
Primary Button
Selected Control
However, in forced colours mode the corresponding system colour should probably be SelectedItem plus SelectedItemText.
Primary Button
Selected Control
This is why it is important to have separate containers defined for selected items.