Skip to content

[SPIKE] Single inverse-text functional colour for white#7178

Draft
romaricpascal wants to merge 9 commits into
mainfrom
spike-inverse-text
Draft

[SPIKE] Single inverse-text functional colour for white#7178
romaricpascal wants to merge 9 commits into
mainfrom
spike-inverse-text

Conversation

@romaricpascal

@romaricpascal romaricpascal commented Jun 16, 2026

Copy link
Copy Markdown
Member

This spike explores adding a single inverse-text functional colour to the components that currently use govuk-colour("white") to set the text over a solid coloured background.

It currently updates:

  • the inverse variant of the links (which trickles down to the pagination link as well)
  • the inverse Breadcrumbs
  • the inverse Service Navigation
  • the Panel (taking the liberty to set its background to the success functional colour
  • the Notification Banner
  • the Header

It steers clear from the Buttons, as it felt like the white in them is better define by button specific functional colours that #6483 will address. File upload is also left out as it could benefit from looking at how its colours are organised first.

Review app page to play around

Findings

Use of link-style-inverse

The initial update of only the inverse links led to the inverse-text colour getting applied across many components. While this sounds great, it highlighted parts of the component that needed extra applications of the inverse-text colour. For example:

  • the Breadcrumbs chevron and non-link text
  • the Service navigation border
Screenshot of components with a custom `inverse-text` colour set, showing the breadcrumbs chevron and non-link text remaining white, as well as the Service navigation border

Move to using link-style-text and --govuk-text-colour

Instead the spike moves away from using govuk-link-style-inverse towards using govuk-link-style-text after updating --govuk-text-colour to be govuk-functional-colour(inverse-text). This enables components using --govuk-text-colour to automatically get the correct colour, and by also configuring the color property, enables elements that don't have a colour defined to pick up the correct colour as well. To further reduce the risks of poor contrast, the spike also explores regrouping the updates of background colour with setting the inverse-text colour.

This creates a neat pattern for adjusting the text colour which could be encapsulated in a mixin:

@mixin govuk-inverse-text($background-colour-name) {
	--govuk-text-colour: #{base.govuk-functional-colour(inverse-text)};
    color: base.govuk-functional-colour(text);
    background: base.govuk-functional-colour($background-colour-name);
}

To keep the text printing as pure black, the --govuk-text-colour needs reseting in @media print. A better option may be to do:

@media print {
	:root {
		--govuk-inverse-text-colour: --govuk-text-colour   
	}
}

It would require us to be sure that backgrounds get set back to transparent when printing, which is not a given (eg. in the Notification Banner, the background remains brand or success).

Resilience to colour changes

Even without touching the buttons, the inverse-text being the same for all backgrounds makes me a little queasy. There's no guarantee that both the brand and success colour (as well as a potential info colour for the Notification Banner) share the same contrast level with inverse-text. If one of brand or success changes so much that inverse-text needs to be changed, that means there's no guarantee that the update to inverse-text will also meet contrast criteria for the other background colour. As we're not white-labelling to cater for any change of colours, this might be OK, but is worth flagging.

Naming is probably a bit meh as it may not necessarily end up being the inverse of the `text` colour,
but that will do for spiking purposes.
This obviously affects the `govuk-link--inverse` class, but also trickles to wherever the `govuk-link-style-inverse` is used, including:
- the header
- the generic header
- the inverse breadcrumbs (however, it only affects the text of the link)
- the service navigation (however it does not affect the border)
- the pagination's current link
Instead of using `link-style-inverse` keep the links `link-style-text` and flip the `--govuk-text-colour`.

This slightly reduces the amount of CSS needed.
Once again, instead of relying on `link-style-inverse`, move to `link-style-text` and updating `--govuk-text-colour` as well as `color`. This ensures that elements not defining a text colour or using `--govuk-text-colour` automatically adjust their text, reducing the risks of poorly contrasted text for users.
Update `--govuk-text-colour` alongside `color` to automatically affect any component injected in the panel.
Move the definition of the background next to the text colour adjustment to reduce risk of contrast issues. Because of how spacing is applied, this meant moving the code to the `__header` element rather than the `__title`.

To coordinate colours between the border and the title background, introduce a local `--govuk-notification-banner-colour`, which also allows to reduce the amount of CSS neeed to swap to a `success` notification banner.
Technically the colour had changed because the header used `link-style-inverse`. However, the intent was to set the text colour of the header to white, so it follows that we use `link-style-text` to pick up on that and reduce the risks of de-synchronising.
@github-actions

Copy link
Copy Markdown

Stylesheets changes to npm package

diff --git a/packages/govuk-frontend/dist/govuk/govuk-frontend.min.css b/packages/govuk-frontend/dist/govuk/govuk-frontend.min.css
index 2e5ab8c44..86c0f953f 100644
--- a/packages/govuk-frontend/dist/govuk/govuk-frontend.min.css
+++ b/packages/govuk-frontend/dist/govuk/govuk-frontend.min.css
@@ -7,6 +7,7 @@
     --govuk-frontend-version: "development";
     --govuk-brand-colour: #1d70b8;
     --govuk-text-colour: #0b0c0c;
+    --govuk-inverse-text-colour: #fff;
     --govuk-template-background-colour: #f4f8fb;
     --govuk-body-background-colour: #fff;
     --govuk-print-text-colour: #000;
@@ -144,7 +145,7 @@
 
 .govuk-link--inverse:link,
 .govuk-link--inverse:visited {
-    color: #fff
+    color: var(--govuk-inverse-text-colour, #fff)
 }
 
 .govuk-link--inverse:focus {
@@ -1507,7 +1508,7 @@
 
 .govuk-back-link--inverse:link,
 .govuk-back-link--inverse:visited {
-    color: #fff
+    color: var(--govuk-inverse-text-colour, #fff)
 }
 
 .govuk-back-link--inverse:focus {
@@ -1659,14 +1660,8 @@
     }
 }
 
-.govuk-breadcrumbs--inverse,
-.govuk-breadcrumbs--inverse .govuk-breadcrumbs__link:link,
-.govuk-breadcrumbs--inverse .govuk-breadcrumbs__link:visited {
-    color: #fff
-}
-
-.govuk-breadcrumbs--inverse .govuk-breadcrumbs__link:focus {
-    color: var(--govuk-focus-text-colour, #0b0c0c)
+.govuk-breadcrumbs--inverse {
+    --govuk-text-colour: var(--govuk-inverse-text-colour, #fff)
 }
 
 .govuk-breadcrumbs--inverse .govuk-breadcrumbs__list-item:before {
@@ -3522,7 +3517,8 @@ screen and (forced-colors:active) {
     font-size: 1rem;
     line-height: 1;
     border-bottom: 1px solid transparent;
-    color: #fff;
+    --govuk-text-colour: var(--govuk-inverse-text-colour, #fff);
+    color: var(--govuk-text-colour, #0b0c0c);
     background: #0b0c0c
 }
 
@@ -3532,7 +3528,7 @@ screen and (forced-colors:active) {
         font-size: 14pt;
         line-height: 1;
         border-bottom-width: 0;
-        color: var(--govuk-text-colour, #0b0c0c);
+        --govuk-text-colour: inherit;
         background: transparent
     }
 }
@@ -3553,9 +3549,14 @@ screen and (forced-colors:active) {
     text-decoration: none
 }
 
+.govuk-generic-header__homepage-link:active,
+.govuk-generic-header__homepage-link:link,
+.govuk-generic-header__homepage-link:visited {
+    color: var(--govuk-text-colour, #0b0c0c)
+}
+
 .govuk-generic-header__homepage-link:link,
 .govuk-generic-header__homepage-link:visited {
-    color: #fff;
     text-decoration: none
 }
 
@@ -3606,7 +3607,8 @@ screen and (forced-colors:active) {
     font-size: 1rem;
     line-height: 1;
     border-bottom: 1px solid transparent;
-    color: #fff;
+    --govuk-text-colour: var(--govuk-inverse-text-colour, #fff);
+    color: var(--govuk-text-colour, #0b0c0c);
     background: var(--govuk-brand-colour, #1d70b8)
 }
 
@@ -3616,7 +3618,7 @@ screen and (forced-colors:active) {
         font-size: 14pt;
         line-height: 1;
         border-bottom-width: 0;
-        color: var(--govuk-text-colour, #0b0c0c);
+        --govuk-text-colour: inherit;
         background: transparent
     }
 }
@@ -3638,9 +3640,14 @@ screen and (forced-colors:active) {
     word-spacing: -.375rem
 }
 
+.govuk-header__homepage-link:active,
+.govuk-header__homepage-link:link,
+.govuk-header__homepage-link:visited {
+    color: var(--govuk-text-colour, #0b0c0c)
+}
+
 .govuk-header__homepage-link:link,
 .govuk-header__homepage-link:visited {
-    color: #fff;
     text-decoration: none
 }
 
@@ -3789,6 +3796,7 @@ screen and (forced-colors:active) {
 }
 
 .govuk-notification-banner {
+    --govuk-notification-banner-colour: var(--govuk-brand-colour, #1d70b8);
     font-family: GDS Transport, arial, sans-serif;
     -webkit-font-smoothing: antialiased;
     -moz-osx-font-smoothing: grayscale;
@@ -3797,8 +3805,7 @@ screen and (forced-colors:active) {
     line-height: 1.3157894737;
     margin-bottom: 30px;
     border: 5px solid;
-    border-color: var(--govuk-brand-colour, #1d70b8);
-    background-color: var(--govuk-brand-colour, #1d70b8)
+    border-color: var(--govuk-notification-banner-colour)
 }
 
 @media print {
@@ -3822,7 +3829,10 @@ screen and (forced-colors:active) {
 
 .govuk-notification-banner__header {
     padding: 2px 15px 5px;
-    border-bottom: 1px solid transparent
+    border-bottom: 1px solid transparent;
+    --govuk-text-colour: var(--govuk-inverse-text-colour, #fff);
+    color: var(--govuk-text-colour, #0b0c0c);
+    background-color: var(--govuk-notification-banner-colour)
 }
 
 @media (min-width:40.0625em) {
@@ -3836,8 +3846,7 @@ screen and (forced-colors:active) {
     line-height: 1.3157894737;
     font-weight: 700;
     margin: 0;
-    padding: 0;
-    color: #fff
+    padding: 0
 }
 
 @media print {
@@ -3945,8 +3954,7 @@ screen and (forced-colors:active) {
 }
 
 .govuk-notification-banner--success {
-    border-color: var(--govuk-success-colour, #0f7a52);
-    background-color: var(--govuk-success-colour, #0f7a52)
+    --govuk-notification-banner-colour: var(--govuk-success-colour, #0f7a52)
 }
 
 .govuk-notification-banner--success .govuk-notification-banner__link:link,
@@ -4071,7 +4079,7 @@ screen and (forced-colors:active) {
 
 .govuk-pagination__item--current .govuk-pagination__link:link,
 .govuk-pagination__item--current .govuk-pagination__link:visited {
-    color: #fff
+    color: var(--govuk-inverse-text-colour, #fff)
 }
 
 .govuk-pagination__item--current .govuk-pagination__link:focus {
@@ -4245,14 +4253,15 @@ screen and (forced-colors:active) {
 }
 
 .govuk-panel--confirmation {
-    color: #fff;
-    background: #0f7a52
+    --govuk-text-colour: var(--govuk-inverse-text-colour, #fff);
+    color: var(--govuk-text-colour, #0b0c0c);
+    background: var(--govuk-success-colour, #0f7a52)
 }
 
 @media print {
     .govuk-panel--confirmation {
+        --govuk-text-colour: inherit;
         border-color: currentcolor;
-        color: var(--govuk-text-colour, #0b0c0c);
         background: none
     }
 }
@@ -4985,7 +4994,8 @@ screen and (forced-colors:active) {
 
 .govuk-service-navigation--inverse {
     border-bottom: none;
-    color: #fff;
+    --govuk-text-colour: var(--govuk-inverse-text-colour, #fff);
+    color: var(--govuk-text-colour, #0b0c0c);
     background-color: var(--govuk-brand-colour, #1d70b8)
 }
 
@@ -5001,12 +5011,13 @@ screen and (forced-colors:active) {
 
 .govuk-service-navigation--inverse .govuk-service-navigation__item,
 .govuk-service-navigation--inverse .govuk-service-navigation__service-name {
-    border-color: #fff
+    border-color: currentcolor
 }
 
+.govuk-service-navigation--inverse .govuk-service-navigation__link:active,
 .govuk-service-navigation--inverse .govuk-service-navigation__link:link,
 .govuk-service-navigation--inverse .govuk-service-navigation__link:visited {
-    color: #fff
+    color: var(--govuk-text-colour, #0b0c0c)
 }
 
 .govuk-service-navigation--inverse .govuk-service-navigation__link:focus {

Action run for c64ee3f

@github-actions

Copy link
Copy Markdown

Other changes to npm package

diff --git a/packages/govuk-frontend/dist/govuk/components/breadcrumbs/_mixin.scss b/packages/govuk-frontend/dist/govuk/components/breadcrumbs/_mixin.scss
index 4b492b04d..8f5e1de89 100644
--- a/packages/govuk-frontend/dist/govuk/components/breadcrumbs/_mixin.scss
+++ b/packages/govuk-frontend/dist/govuk/components/breadcrumbs/_mixin.scss
@@ -124,11 +124,7 @@
   }
 
   .govuk-breadcrumbs--inverse {
-    color: base.govuk-colour("white");
-
-    .govuk-breadcrumbs__link {
-      @include base.govuk-link-style-inverse;
-    }
+    --govuk-text-colour: #{base.govuk-functional-colour(inverse-text)};
 
     .govuk-breadcrumbs__list-item::before {
       border-color: currentcolor;
diff --git a/packages/govuk-frontend/dist/govuk/components/generic-header/_mixin.scss b/packages/govuk-frontend/dist/govuk/components/generic-header/_mixin.scss
index 577b312b6..d1ce8a874 100644
--- a/packages/govuk-frontend/dist/govuk/components/generic-header/_mixin.scss
+++ b/packages/govuk-frontend/dist/govuk/components/generic-header/_mixin.scss
@@ -20,12 +20,15 @@ $header-forced-colour-border-width: 1px;
 
   // Add a transparent bottom border for forced-colour modes
   border-bottom: $header-forced-colour-border-width solid transparent;
-  color: base.govuk-colour("white");
+
+  --govuk-text-colour: #{base.govuk-functional-colour(inverse-text)};
+  color: base.govuk-functional-colour(text);
   background: $background;
 
   @media print {
     border-bottom-width: 0;
-    color: base.govuk-functional-colour(text);
+
+    --govuk-text-colour: inherit;
     background: transparent;
   }
 }
@@ -73,7 +76,7 @@ $header-forced-colour-border-width: 1px;
   font-size: 30px; // We don't have a mixin that produces 30px font size
   text-decoration: none;
 
-  @include base.govuk-link-style-inverse;
+  @include base.govuk-link-style-text;
 
   &:link,
   &:visited {
diff --git a/packages/govuk-frontend/dist/govuk/components/notification-banner/_mixin.scss b/packages/govuk-frontend/dist/govuk/components/notification-banner/_mixin.scss
index f892ead6f..f48cd70e9 100644
--- a/packages/govuk-frontend/dist/govuk/components/notification-banner/_mixin.scss
+++ b/packages/govuk-frontend/dist/govuk/components/notification-banner/_mixin.scss
@@ -5,13 +5,13 @@
 /// @access private
 @mixin styles {
   .govuk-notification-banner {
+    --govuk-notification-banner-colour: #{base.govuk-functional-colour(brand)};
+
     @include base.govuk-font($size: 19);
     @include base.govuk-responsive-margin(8, "bottom");
 
     border: base.$govuk-border-width solid;
-    border-color: base.govuk-functional-colour(brand);
-
-    background-color: base.govuk-functional-colour(brand);
+    border-color: var(--govuk-notification-banner-colour);
 
     &:focus {
       outline: base.$govuk-focus-width solid;
@@ -26,6 +26,10 @@
     // text in high contrast mode
     border-bottom: 1px solid transparent;
 
+    --govuk-text-colour: #{base.govuk-functional-colour(inverse-text)};
+    color: base.govuk-functional-colour(text);
+    background-color: var(--govuk-notification-banner-colour);
+
     @media #{base.govuk-from-breakpoint(tablet)} {
       padding: 2px base.govuk-spacing(4) base.govuk-spacing(1);
     }
@@ -38,7 +42,6 @@
     @include base.govuk-typography-weight-bold;
     margin: 0;
     padding: 0;
-    color: base.govuk-colour("white");
   }
 
   .govuk-notification-banner__content {
@@ -87,9 +90,7 @@
   }
 
   .govuk-notification-banner--success {
-    border-color: base.govuk-functional-colour(success);
-
-    background-color: base.govuk-functional-colour(success);
+    --govuk-notification-banner-colour: #{base.govuk-functional-colour(success)};
 
     .govuk-notification-banner__link {
       @include base.govuk-link-style-success;
diff --git a/packages/govuk-frontend/dist/govuk/components/panel/_mixin.scss b/packages/govuk-frontend/dist/govuk/components/panel/_mixin.scss
index 566c3998c..34ae42899 100644
--- a/packages/govuk-frontend/dist/govuk/components/panel/_mixin.scss
+++ b/packages/govuk-frontend/dist/govuk/components/panel/_mixin.scss
@@ -36,12 +36,13 @@
   }
 
   .govuk-panel--confirmation {
-    color: base.govuk-colour("white");
-    background: base.govuk-colour("green");
+    --govuk-text-colour: #{base.govuk-functional-colour(inverse-text)};
+    color: base.govuk-functional-colour(text);
+    background: base.govuk-functional-colour(success);
 
     @media print {
+      --govuk-text-colour: inherit;
       border-color: currentcolor;
-      color: base.govuk-functional-colour(text);
       background: none;
     }
   }
diff --git a/packages/govuk-frontend/dist/govuk/components/service-navigation/_mixin.scss b/packages/govuk-frontend/dist/govuk/components/service-navigation/_mixin.scss
index 96d26fc6c..f05f59260 100644
--- a/packages/govuk-frontend/dist/govuk/components/service-navigation/_mixin.scss
+++ b/packages/govuk-frontend/dist/govuk/components/service-navigation/_mixin.scss
@@ -202,7 +202,8 @@
 
     // Set colour here so non-link text (service name, slot content) can
     // use it too.
-    color: base.govuk-colour("white");
+    --govuk-text-colour: #{base.govuk-functional-colour(inverse-text)};
+    color: base.govuk-functional-colour(text);
 
     background-color: base.govuk-functional-colour(brand);
 
@@ -220,12 +221,12 @@
     // Override the 'active' border colour
     .govuk-service-navigation__item,
     .govuk-service-navigation__service-name {
-      border-color: base.govuk-colour("white");
+      border-color: currentcolor;
     }
 
     // Override link styles
     .govuk-service-navigation__link {
-      @include base.govuk-link-style-inverse;
+      @include base.govuk-link-style-text;
     }
 
     // Override mobile menu toggle colour when not focused
diff --git a/packages/govuk-frontend/dist/govuk/helpers/_links.scss b/packages/govuk-frontend/dist/govuk/helpers/_links.scss
index 332854078..2cc324f91 100644
--- a/packages/govuk-frontend/dist/govuk/helpers/_links.scss
+++ b/packages/govuk-frontend/dist/govuk/helpers/_links.scss
@@ -259,7 +259,7 @@
 @mixin govuk-link-style-inverse {
   &:link,
   &:visited {
-    color: govuk-colour("white");
+    color: govuk-functional-colour(inverse-text);
   }
 
   &:focus {
diff --git a/packages/govuk-frontend/dist/govuk/settings/_colours-functional.scss b/packages/govuk-frontend/dist/govuk/settings/_colours-functional.scss
index 024b61419..21848f087 100644
--- a/packages/govuk-frontend/dist/govuk/settings/_colours-functional.scss
+++ b/packages/govuk-frontend/dist/govuk/settings/_colours-functional.scss
@@ -20,6 +20,9 @@ $govuk-default-functional-colours: (
     "text": (
       name: "black"
     ),
+    "inverse-text": (
+      name: "white"
+    ),
     // The background colour of the template. This is intended to be the same
     // as `surface-background` for the purposes of making the Footer and Cookie
     // banner components merge seamlessly with the template.

Action run for c64ee3f

@github-actions

Copy link
Copy Markdown

📋 Stats

File sizes

File Size Percentage change
dist/govuk-frontend-development.min.css 122.27 KiB 0.7%
packages/govuk-frontend/dist/govuk/govuk-frontend.min.css 122.26 KiB 0.7%

No changes to module sizes.


Action run for c64ee3f

@NickColley

Copy link
Copy Markdown
Contributor

Setting the --govuk-text-colour makes a lot of sense to me, since that is the 'hook'.

If one of brand or success changes so much that inverse-text needs to be changed, that means there's no guarantee that the update to inverse-text will also meet contrast criteria for the other background colour.

We have discussed separately that perhaps in the short term making it clear that some colours e.g. must be dark and contrast well with white is an okay compromise. Alternatively it makes sense to have say a new functional colour that defaults to inverse-text e.g. brand-text


// Override link styles
.govuk-service-navigation__link {
@include base.govuk-link-style-inverse;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this makes sense since what we've seen from the App team is they do have an idea of a dark mode link colour e.g. they use the bright cyan colour but our 'inverse' theme doesnt distinguish between links and text colourwise...

&:link,
&:visited {
color: govuk-colour("white");
color: govuk-functional-colour(inverse-text);

@NickColley NickColley Jun 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this make sense or is it more appropriate to be doing something like:

@mixin govuk-link-style-inverse {
  --govuk-link-colour: --govuk-inverse-text-colour;
  --govuk-link-visited-colour: --govuk-inverse-text-colour;
  /* etc */
}

Base automatically changed from generic-header to main June 17, 2026 08:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Explore impact of using a single inverse-text functional colour for "white against solid background"

3 participants