Skip to content

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/Panel
Layer/Background/Primary
Layer/Background/Sentiment
Foreground
Layer/Foreground/Panel
Layer/Foreground/Primary
Layer/Foreground/Sentiment
Border
Layer/Border/Panel
Layer/Border/Primary
Layer/Border/Sentiment
Container
Standard
Disabled
Background
Transparent
Semantics/Disabled/Background
Semantics/Disabled/Background
Foreground
Semantics/Disabled/Foreground
Semantics/Disabled/Foreground
Semantics/Disabled/Foreground
Border
Semantics/Disabled/Border
Semantics/Disabled/Border
Semantics/Disabled/Border
Container
Forced Colours
Active
Background
System/Canvas
System/ButtonFace
System/ButtonFace
Foreground
System/CanvasText
System/ButtonText
System/ButtonText
Border
System/CanvasText
System/ButtonBorder
System/ButtonBorder
Container
Forced Colours
Disabled
Background
Transparent
Transparent
Transparent
Foreground
System/GrayText
System/GrayText
System/GrayText
Border
System/GrayText
System/GrayText
System/GrayText

We 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/Background
Container/Forced Colours/Active/Background
Foreground
Container/Standard/Active/Foreground
Container/Forced Colours/Active/Foreground
Border
Container/Standard/Active/Border
Container/Forced Colours/Active/Border
Container
Disabled
Background
Container/Standard/Disabled/Background
Container/Forced Colours/Disabled/Background
Foreground
Container/Standard/Disabled/Foreground
Container/Forced Colours/Disabled/Foreground
Border
Container/Standard/Disabled/Border
Container/Forced Colours/Disabled/Border

Before our final layer that selects between Active/Disabled states:

Container State
Name
Active
Disabled
Container
Background
Container/Active/Background
Container/Disabled/Background
Foreground
Container/Active/Foreground
Container/Disabled/Foreground
Border
Container/Active/Border
Container/Disabled/Border
Active
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.