Skip to content

[SPIKE] Link variants through custom properties (selectors in govuk-link-style-*)#7247

Draft
romaricpascal wants to merge 3 commits into
language-switcher-and-inverse-textfrom
spike-link-custom-properties-selectors-in-style-mixins
Draft

[SPIKE] Link variants through custom properties (selectors in govuk-link-style-*)#7247
romaricpascal wants to merge 3 commits into
language-switcher-and-inverse-textfrom
spike-link-custom-properties-selectors-in-style-mixins

Conversation

@romaricpascal

@romaricpascal romaricpascal commented Jul 3, 2026

Copy link
Copy Markdown
Member

This spike is one of two exploring the use of custom properties to change the colours of the link variants, particularly to allow a parent element to configure the colours of all the links inside it, which is useful for the Service Navigation.

Ideally, links across the markup would use .govuk-link and the variants would only have to set the relevant --govuk-link(-<STATE>)-colour custom properties (see first commit). This is great, but only affects markup actually using .govuk-link.

In the couple of instances where links are styles using mixins attached to a completely different class (for example the header's homepage link or the error summary). These instances need both:

  • to 'theme' the links by applying the right colours to the --govuk-link(-<STATE>)-colour
  • to 'style' the links attaching the relevant --govuk-link-<STATE>-colour to the :<STATE> selector

Otherwise the <a> element get the browser's default styles:

Error summary showing the default link styles Generic header showing the default link styles

To avoid breaking changes, the simplest route is to keep our govuk-link-style-<STYLE> mixins output both selectors and changes of custom properties. To facilitate reuse of which variables need changing for a given style, we can extract the variable changes in their own govuk-link-theme-<STYLE> mixin, starting with the -no-visited-state one that all the other styles use. (see second commit).

This reduces the gains in CSS output, but still opens the path to parents styling links through case. By extracting a theme for text links, the Service Navigation is able to style the links in its inverse variant from the .govuk-service-navigation--inverse selector, reducing the need for extra selectors to affect:

  • the link of the service name
  • the links of the navigation
  • any .govuk-link or class using govuk-link-style-default injected in its slot.

See third commit.

Thoughts

Moving to custom properties enables to let the link styles cascade to descendants of components, which facilitate creating --inverse variants.

Keeping selectors output by each of the --govuk-link-style-<STYLE> mixins to avoid a breaking change is a bit sad, as we don't get as much gain on the CSS output. Down the line, I'd be keen to rework our components to use .govuk-link whenever they need a link, which would allow the custom classes to only set custom properties to change the colour of the link.

Having the output of selectors in each --govuk-link-style-<STYLE> mixin also requires us to have separate --govuk-link-theme-<STYLE> mixins for components to set the colours. This doubles the number of mixins to manage, and creates a nuance between 'style' and 'theme' which may be tricky to grasp.

Makes link variants override `--govuk-link-colour`, `--govuk-link-hover-colour` and `--govuk-link-active-colour`,
as well as include `govuk-link-style-no-visited-state` instead of redefining selectors.

This not only avoids having each variant create selectors for each of the state, but removes the need to duplicate the `:focus` in each of them too, only switching the colours that need switching.

It also allows to better define the relationships between the colours, explicitely saying that the active state is the same as the link colour, rather than duplicating the colours.
If the `govuk-link-style-<STYLE_NAME>` mixins only set the custom properties,
the link style doesn't change in classes set on elements that are not also `.govuk-link`.
For example, the Header's link, or the Error Summary's links would not have the correct styles.

This is because, previously, the `govuk-link-style-<STYLE_NAME>` mixins were creating selectors
for each of the link's state. To avoid a breaking change, we need to restore that, which we can do
by applying `govuk-link-style-default` in all mixins.

To avoid doubling the markup in a future PR, we separate:
- applying the styles in `govuk-link-style...`, meant for use on link elements
- themeing the links by applying custom properties, in a new `govuk-link-theme...` mixin
As link states now rely on custom properties, we can use them to adjust the colours of the links in the Service Navigation more easily.

When not inverted, we no longer need a compound selector for adjusting the link colour. The `__service-name` element can set up the appropriate custom properties and let them cascade to the `__link` element it contains.

When inverted, the Service Navigation itself can set up the right custom properties to style all links as text, configuring in one go:
- the link of the Service Name
- the links of the navigation itself
- any link injected in slots, like links from a Language Switcher

Because we need to set the custom properties on a parent, we need to extract a `govuk-link-theme-text` mixin to apply only the custom properties.
@github-actions

github-actions Bot commented Jul 3, 2026

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 ef495c34f..25f88b972 100644
--- a/packages/govuk-frontend/dist/govuk/govuk-frontend.min.css
+++ b/packages/govuk-frontend/dist/govuk/govuk-frontend.min.css
@@ -119,33 +119,81 @@
     }
 }
 
-.govuk-link--muted:link,
+.govuk-link--muted {
+    --govuk-link-visited-colour: var(--govuk-link-colour);
+    --govuk-link-colour: var(--govuk-secondary-text-colour, #484949);
+    --govuk-link-hover-colour: var(--govuk-text-colour, #0b0c0c);
+    --govuk-link-active-colour: var(--govuk-link-hover-colour)
+}
+
+.govuk-link--muted:link {
+    color: var(--govuk-link-colour, #1a65a6)
+}
+
 .govuk-link--muted:visited {
-    color: var(--govuk-secondary-text-colour, #484949)
+    color: var(--govuk-link-visited-colour, #54319f)
 }
 
-.govuk-link--muted:active,
 .govuk-link--muted:hover {
-    color: var(--govuk-text-colour, #0b0c0c)
+    color: var(--govuk-link-hover-colour, #0f385c)
+}
+
+.govuk-link--muted:active {
+    color: var(--govuk-link-active-colour, #0b0c0c)
 }
 
 .govuk-link--muted:focus {
     color: var(--govuk-focus-text-colour, #0b0c0c)
 }
 
-.govuk-link--text-colour:active,
-.govuk-link--text-colour:link,
+.govuk-link--text-colour {
+    --govuk-link-visited-colour: var(--govuk-link-colour);
+    --govuk-link-colour: var(--govuk-text-colour, #0b0c0c);
+    --govuk-link-hover-colour: var(--govuk-link-colour);
+    --govuk-link-active-colour: var(--govuk-link-colour)
+}
+
+.govuk-link--text-colour:link {
+    color: var(--govuk-link-colour, #1a65a6)
+}
+
 .govuk-link--text-colour:visited {
-    color: var(--govuk-text-colour, #0b0c0c)
+    color: var(--govuk-link-visited-colour, #54319f)
+}
+
+.govuk-link--text-colour:hover {
+    color: var(--govuk-link-hover-colour, #0f385c)
+}
+
+.govuk-link--text-colour:active {
+    color: var(--govuk-link-active-colour, #0b0c0c)
 }
 
 .govuk-link--text-colour:focus {
     color: var(--govuk-focus-text-colour, #0b0c0c)
 }
 
-.govuk-link--inverse:link,
+.govuk-link--inverse {
+    --govuk-link-visited-colour: var(--govuk-link-colour);
+    --govuk-link-colour: var(--govuk-inverse-text-colour, #fff);
+    --govuk-link-hover-colour: var(--govuk-link-colour);
+    --govuk-link-active-colour: var(--govuk-link-colour)
+}
+
+.govuk-link--inverse:link {
+    color: var(--govuk-link-colour, #1a65a6)
+}
+
 .govuk-link--inverse:visited {
-    color: var(--govuk-inverse-text-colour, #fff)
+    color: var(--govuk-link-visited-colour, #54319f)
+}
+
+.govuk-link--inverse:hover {
+    color: var(--govuk-link-hover-colour, #0f385c)
+}
+
+.govuk-link--inverse:active {
+    color: var(--govuk-link-active-colour, #0b0c0c)
 }
 
 .govuk-link--inverse:focus {
@@ -156,11 +204,18 @@
     text-decoration: none
 }
 
-.govuk-link--no-visited-state:link,
-.govuk-link--no-visited-state:visited {
+.govuk-link--no-visited-state {
+    --govuk-link-visited-colour: var(--govuk-link-colour)
+}
+
+.govuk-link--no-visited-state:link {
     color: var(--govuk-link-colour, #1a65a6)
 }
 
+.govuk-link--no-visited-state:visited {
+    color: var(--govuk-link-visited-colour, #54319f)
+}
+
 .govuk-link--no-visited-state:hover {
     color: var(--govuk-link-hover-colour, #0f385c)
 }
@@ -1423,6 +1478,10 @@
     text-decoration: underline;
     text-decoration-thickness: max(1px, .0625rem);
     text-underline-offset: .1578em;
+    --govuk-link-visited-colour: var(--govuk-link-colour);
+    --govuk-link-colour: var(--govuk-text-colour, #0b0c0c);
+    --govuk-link-hover-colour: var(--govuk-link-colour);
+    --govuk-link-active-colour: var(--govuk-link-colour);
     display: inline-block;
     position: relative;
     margin-top: 15px;
@@ -1460,10 +1519,20 @@
     }
 }
 
-.govuk-back-link:active,
-.govuk-back-link:link,
+.govuk-back-link:link {
+    color: var(--govuk-link-colour, #1a65a6)
+}
+
 .govuk-back-link:visited {
-    color: var(--govuk-text-colour, #0b0c0c)
+    color: var(--govuk-link-visited-colour, #54319f)
+}
+
+.govuk-back-link:hover {
+    color: var(--govuk-link-hover-colour, #0f385c)
+}
+
+.govuk-back-link:active {
+    color: var(--govuk-link-active-colour, #0b0c0c)
 }
 
 .govuk-back-link:focus {
@@ -1506,9 +1575,27 @@
     left: 0
 }
 
-.govuk-back-link--inverse:link,
+.govuk-back-link--inverse {
+    --govuk-link-visited-colour: var(--govuk-link-colour);
+    --govuk-link-colour: var(--govuk-inverse-text-colour, #fff);
+    --govuk-link-hover-colour: var(--govuk-link-colour);
+    --govuk-link-active-colour: var(--govuk-link-colour)
+}
+
+.govuk-back-link--inverse:link {
+    color: var(--govuk-link-colour, #1a65a6)
+}
+
 .govuk-back-link--inverse:visited {
-    color: var(--govuk-inverse-text-colour, #fff)
+    color: var(--govuk-link-visited-colour, #54319f)
+}
+
+.govuk-back-link--inverse:hover {
+    color: var(--govuk-link-hover-colour, #0f385c)
+}
+
+.govuk-back-link--inverse:active {
+    color: var(--govuk-link-active-colour, #0b0c0c)
 }
 
 .govuk-back-link--inverse:focus {
@@ -1599,7 +1686,11 @@
     -moz-osx-font-smoothing: grayscale;
     text-decoration: underline;
     text-decoration-thickness: max(1px, .0625rem);
-    text-underline-offset: .1578em
+    text-underline-offset: .1578em;
+    --govuk-link-visited-colour: var(--govuk-link-colour);
+    --govuk-link-colour: var(--govuk-text-colour, #0b0c0c);
+    --govuk-link-hover-colour: var(--govuk-link-colour);
+    --govuk-link-active-colour: var(--govuk-link-colour)
 }
 
 @media print {
@@ -1630,10 +1721,20 @@
     }
 }
 
-.govuk-breadcrumbs__link:active,
-.govuk-breadcrumbs__link:link,
+.govuk-breadcrumbs__link:link {
+    color: var(--govuk-link-colour, #1a65a6)
+}
+
 .govuk-breadcrumbs__link:visited {
-    color: var(--govuk-text-colour, #0b0c0c)
+    color: var(--govuk-link-visited-colour, #54319f)
+}
+
+.govuk-breadcrumbs__link:hover {
+    color: var(--govuk-link-hover-colour, #0f385c)
+}
+
+.govuk-breadcrumbs__link:active {
+    color: var(--govuk-link-active-colour, #0b0c0c)
 }
 
 .govuk-breadcrumbs__link:focus {
@@ -2918,7 +3019,11 @@ screen and (forced-colors:active) {
     -moz-osx-font-smoothing: grayscale;
     text-decoration: underline;
     text-decoration-thickness: max(1px, .0625rem);
-    text-underline-offset: .1578em
+    text-underline-offset: .1578em;
+    --govuk-link-visited-colour: var(--govuk-link-colour);
+    --govuk-link-colour: var(--govuk-error-colour, #ca3535);
+    --govuk-link-hover-colour: #651b1b;
+    --govuk-link-active-colour: var(--govuk-link-colour)
 }
 
 @media print {
@@ -2949,17 +3054,20 @@ screen and (forced-colors:active) {
     }
 }
 
-.govuk-error-summary__list a:link,
+.govuk-error-summary__list a:link {
+    color: var(--govuk-link-colour, #1a65a6)
+}
+
 .govuk-error-summary__list a:visited {
-    color: var(--govuk-error-colour, #ca3535)
+    color: var(--govuk-link-visited-colour, #54319f)
 }
 
 .govuk-error-summary__list a:hover {
-    color: #651b1b
+    color: var(--govuk-link-hover-colour, #0f385c)
 }
 
 .govuk-error-summary__list a:active {
-    color: var(--govuk-error-colour, #ca3535)
+    color: var(--govuk-link-active-colour, #0b0c0c)
 }
 
 .govuk-error-summary__list a:focus {
@@ -3384,10 +3492,27 @@ screen and (forced-colors:active) {
     text-wrap: balance
 }
 
-.govuk-footer__copyright-logo:active,
-.govuk-footer__copyright-logo:link,
+.govuk-footer__copyright-logo {
+    --govuk-link-visited-colour: var(--govuk-link-colour);
+    --govuk-link-colour: var(--govuk-text-colour, #0b0c0c);
+    --govuk-link-hover-colour: var(--govuk-link-colour);
+    --govuk-link-active-colour: var(--govuk-link-colour)
+}
+
+.govuk-footer__copyright-logo:link {
+    color: var(--govuk-link-colour, #1a65a6)
+}
+
 .govuk-footer__copyright-logo:visited {
-    color: var(--govuk-text-colour, #0b0c0c)
+    color: var(--govuk-link-visited-colour, #54319f)
+}
+
+.govuk-footer__copyright-logo:hover {
+    color: var(--govuk-link-hover-colour, #0f385c)
+}
+
+.govuk-footer__copyright-logo:active {
+    color: var(--govuk-link-active-colour, #0b0c0c)
 }
 
 .govuk-footer__copyright-logo:focus {
@@ -3548,13 +3673,27 @@ screen and (forced-colors:active) {
     display: inline;
     margin-right: 10px;
     font-size: 30px;
-    text-decoration: none
+    text-decoration: none;
+    --govuk-link-visited-colour: var(--govuk-link-colour);
+    --govuk-link-colour: var(--govuk-text-colour, #0b0c0c);
+    --govuk-link-hover-colour: var(--govuk-link-colour);
+    --govuk-link-active-colour: var(--govuk-link-colour)
+}
+
+.govuk-generic-header__homepage-link:link {
+    color: var(--govuk-link-colour, #1a65a6)
 }
 
-.govuk-generic-header__homepage-link:active,
-.govuk-generic-header__homepage-link:link,
 .govuk-generic-header__homepage-link:visited {
-    color: var(--govuk-text-colour, #0b0c0c)
+    color: var(--govuk-link-visited-colour, #54319f)
+}
+
+.govuk-generic-header__homepage-link:hover {
+    color: var(--govuk-link-hover-colour, #0f385c)
+}
+
+.govuk-generic-header__homepage-link:active {
+    color: var(--govuk-link-active-colour, #0b0c0c)
 }
 
 .govuk-generic-header__homepage-link:link,
@@ -3639,13 +3778,27 @@ screen and (forced-colors:active) {
     margin-right: 10px;
     font-size: 30px;
     text-decoration: none;
+    --govuk-link-visited-colour: var(--govuk-link-colour);
+    --govuk-link-colour: var(--govuk-text-colour, #0b0c0c);
+    --govuk-link-hover-colour: var(--govuk-link-colour);
+    --govuk-link-active-colour: var(--govuk-link-colour);
     word-spacing: -.375rem
 }
 
-.govuk-header__homepage-link:active,
-.govuk-header__homepage-link:link,
+.govuk-header__homepage-link:link {
+    color: var(--govuk-link-colour, #1a65a6)
+}
+
 .govuk-header__homepage-link:visited {
-    color: var(--govuk-text-colour, #0b0c0c)
+    color: var(--govuk-link-visited-colour, #54319f)
+}
+
+.govuk-header__homepage-link:hover {
+    color: var(--govuk-link-hover-colour, #0f385c)
+}
+
+.govuk-header__homepage-link:active {
+    color: var(--govuk-link-active-colour, #0b0c0c)
 }
 
 .govuk-header__homepage-link:link,
@@ -4008,7 +4161,8 @@ screen and (forced-colors:active) {
     -moz-osx-font-smoothing: grayscale;
     text-decoration: underline;
     text-decoration-thickness: max(1px, .0625rem);
-    text-underline-offset: .1578em
+    text-underline-offset: .1578em;
+    --govuk-link-visited-colour: var(--govuk-link-colour)
 }
 
 @media print {
@@ -4039,11 +4193,14 @@ screen and (forced-colors:active) {
     }
 }
 
-.govuk-notification-banner__link:link,
-.govuk-notification-banner__link:visited {
+.govuk-notification-banner__link:link {
     color: var(--govuk-link-colour, #1a65a6)
 }
 
+.govuk-notification-banner__link:visited {
+    color: var(--govuk-link-visited-colour, #54319f)
+}
+
 .govuk-notification-banner__link:hover {
     color: var(--govuk-link-hover-colour, #0f385c)
 }
@@ -4060,17 +4217,27 @@ screen and (forced-colors:active) {
     --_govuk-notification-banner-colour: var(--govuk-success-colour, #0f7a52)
 }
 
-.govuk-notification-banner--success .govuk-notification-banner__link:link,
+.govuk-notification-banner--success .govuk-notification-banner__link {
+    --govuk-link-visited-colour: var(--govuk-link-colour);
+    --govuk-link-colour: var(--govuk-success-colour, #0f7a52);
+    --govuk-link-hover-colour: #083d29;
+    --govuk-link-active-colour: var(--govuk-link-colour)
+}
+
+.govuk-notification-banner--success .govuk-notification-banner__link:link {
+    color: var(--govuk-link-colour, #1a65a6)
+}
+
 .govuk-notification-banner--success .govuk-notification-banner__link:visited {
-    color: var(--govuk-success-colour, #0f7a52)
+    color: var(--govuk-link-visited-colour, #54319f)
 }
 
 .govuk-notification-banner--success .govuk-notification-banner__link:hover {
-    color: #083d29
+    color: var(--govuk-link-hover-colour, #0f385c)
 }
 
 .govuk-notification-banner--success .govuk-notification-banner__link:active {
-    color: var(--govuk-success-colour, #0f7a52)
+    color: var(--govuk-link-active-colour, #0b0c0c)
 }
 
 .govuk-notification-banner--success .govuk-notification-banner__link:focus {
@@ -4180,9 +4347,27 @@ screen and (forced-colors:active) {
     background-color: var(--govuk-link-colour, #1a65a6)
 }
 
-.govuk-pagination__item--current .govuk-pagination__link:link,
+.govuk-pagination__item--current .govuk-pagination__link {
+    --govuk-link-visited-colour: var(--govuk-link-colour);
+    --govuk-link-colour: var(--govuk-inverse-text-colour, #fff);
+    --govuk-link-hover-colour: var(--govuk-link-colour);
+    --govuk-link-active-colour: var(--govuk-link-colour)
+}
+
+.govuk-pagination__item--current .govuk-pagination__link:link {
+    color: var(--govuk-link-colour, #1a65a6)
+}
+
 .govuk-pagination__item--current .govuk-pagination__link:visited {
-    color: var(--govuk-inverse-text-colour, #fff)
+    color: var(--govuk-link-visited-colour, #54319f)
+}
+
+.govuk-pagination__item--current .govuk-pagination__link:hover {
+    color: var(--govuk-link-hover-colour, #0f385c)
+}
+
+.govuk-pagination__item--current .govuk-pagination__link:active {
+    color: var(--govuk-link-active-colour, #0b0c0c)
 }
 
 .govuk-pagination__item--current .govuk-pagination__link:focus {
@@ -4922,7 +5107,8 @@ screen and (forced-colors:active) {
     -moz-osx-font-smoothing: grayscale;
     text-decoration: underline;
     text-decoration-thickness: max(1px, .0625rem);
-    text-underline-offset: .1578em
+    text-underline-offset: .1578em;
+    --govuk-link-visited-colour: var(--govuk-link-colour)
 }
 
 @media print {
@@ -4957,11 +5143,14 @@ screen and (forced-colors:active) {
     text-decoration: none
 }
 
-.govuk-service-navigation__link:link,
-.govuk-service-navigation__link:visited {
+.govuk-service-navigation__link:link {
     color: var(--govuk-link-colour, #1a65a6)
 }
 
+.govuk-service-navigation__link:visited {
+    color: var(--govuk-link-visited-colour, #54319f)
+}
+
 .govuk-service-navigation__link:hover {
     color: var(--govuk-link-hover-colour, #0f385c)
 }
@@ -4975,17 +5164,11 @@ screen and (forced-colors:active) {
 }
 
 .govuk-service-navigation__service-name {
-    font-weight: 700
-}
-
-.govuk-service-navigation__service-name .govuk-service-navigation__link:active,
-.govuk-service-navigation__service-name .govuk-service-navigation__link:link,
-.govuk-service-navigation__service-name .govuk-service-navigation__link:visited {
-    color: var(--govuk-text-colour, #0b0c0c)
-}
-
-.govuk-service-navigation__service-name .govuk-service-navigation__link:focus {
-    color: var(--govuk-focus-text-colour, #0b0c0c)
+    font-weight: 700;
+    --govuk-link-visited-colour: var(--govuk-link-colour);
+    --govuk-link-colour: var(--govuk-text-colour, #0b0c0c);
+    --govuk-link-hover-colour: var(--govuk-link-colour);
+    --govuk-link-active-colour: var(--govuk-link-colour)
 }
 
 .govuk-service-navigation__wrapper {
@@ -5110,7 +5293,12 @@ screen and (forced-colors:active) {
 .govuk-service-navigation--inverse {
     border-bottom: none;
     --govuk-text-colour: var(--govuk-inverse-text-colour, #fff);
-    background-color: var(--govuk-brand-colour, #1d70b8)
+    background-color: var(--govuk-brand-colour, #1d70b8);
+    --govuk-link-visited-colour: var(--govuk-link-colour);
+    --govuk-link-colour: var(--govuk-text-colour, #0b0c0c);
+    --govuk-link-hover-colour: var(--govuk-link-colour);
+    --govuk-link-active-colour: var(--govuk-link-colour);
+    --govuk-secondary-text-colour: currentcolor
 }
 
 .govuk-service-navigation--inverse .govuk-width-container {
@@ -5128,16 +5316,6 @@ screen and (forced-colors:active) {
     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: var(--govuk-text-colour, #0b0c0c)
-}
-
-.govuk-service-navigation--inverse .govuk-service-navigation__link:focus {
-    color: var(--govuk-focus-text-colour, #0b0c0c)
-}
-
 .govuk-service-navigation--inverse .govuk-service-navigation__toggle:not(:focus) {
     color: currentcolor
 }
@@ -5146,6 +5324,10 @@ screen and (forced-colors:active) {
     text-decoration: underline;
     text-decoration-thickness: max(1px, .0625rem);
     text-underline-offset: .1578em;
+    --govuk-link-visited-colour: var(--govuk-link-colour);
+    --govuk-link-colour: var(--govuk-text-colour, #0b0c0c);
+    --govuk-link-hover-colour: var(--govuk-link-colour);
+    --govuk-link-active-colour: var(--govuk-link-colour);
     font-family: GDS Transport, arial, sans-serif;
     -webkit-font-smoothing: antialiased;
     -moz-osx-font-smoothing: grayscale;
@@ -5173,10 +5355,20 @@ screen and (forced-colors:active) {
     user-select: none
 }
 
-.govuk-skip-link:active,
-.govuk-skip-link:link,
+.govuk-skip-link:link {
+    color: var(--govuk-link-colour, #1a65a6)
+}
+
 .govuk-skip-link:visited {
-    color: var(--govuk-text-colour, #0b0c0c)
+    color: var(--govuk-link-visited-colour, #54319f)
+}
+
+.govuk-skip-link:hover {
+    color: var(--govuk-link-hover-colour, #0f385c)
+}
+
+.govuk-skip-link:active {
+    color: var(--govuk-link-active-colour, #0b0c0c)
 }
 
 .govuk-skip-link:focus {
@@ -5843,13 +6035,27 @@ screen and (-ms-high-contrast:active) {
     }
 
     .govuk-frontend-supported .govuk-tabs__tab {
+        --govuk-link-visited-colour: var(--govuk-link-colour);
+        --govuk-link-colour: var(--govuk-text-colour, #0b0c0c);
+        --govuk-link-hover-colour: var(--govuk-link-colour);
+        --govuk-link-active-colour: var(--govuk-link-colour);
         margin-bottom: 0
     }
 
-    .govuk-frontend-supported .govuk-tabs__tab:active,
-    .govuk-frontend-supported .govuk-tabs__tab:link,
+    .govuk-frontend-supported .govuk-tabs__tab:link {
+        color: var(--govuk-link-colour, #1a65a6)
+    }
+
     .govuk-frontend-supported .govuk-tabs__tab:visited {
-        color: var(--govuk-text-colour, #0b0c0c)
+        color: var(--govuk-link-visited-colour, #54319f)
+    }
+
+    .govuk-frontend-supported .govuk-tabs__tab:hover {
+        color: var(--govuk-link-hover-colour, #0f385c)
+    }
+
+    .govuk-frontend-supported .govuk-tabs__tab:active {
+        color: var(--govuk-link-active-colour, #0b0c0c)
     }
 
     .govuk-frontend-supported .govuk-tabs__tab:focus {

Action run for fce53c6

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

Rendered HTML changes to npm package

diff --git a/packages/govuk-frontend/dist/govuk/components/service-navigation/template-with-language-switcher-inverse.html b/packages/govuk-frontend/dist/govuk/components/service-navigation/template-with-language-switcher-inverse.html
new file mode 100644
index 000000000..60c0fe057
--- /dev/null
+++ b/packages/govuk-frontend/dist/govuk/components/service-navigation/template-with-language-switcher-inverse.html
@@ -0,0 +1,66 @@
+
+  <section aria-label="Service information" class="govuk-service-navigation govuk-service-navigation--inverse"
+data-module="govuk-service-navigation"
+>
+      <div class="govuk-width-container">
+
+    <div class="govuk-service-navigation__container">
+      
+        <span class="govuk-service-navigation__service-name">
+            <a href="#/" class="govuk-service-navigation__link">
+              Service name
+            </a>
+        </span>
+
+      
+        <nav aria-label="Menu" class="govuk-service-navigation__wrapper">
+          <button type="button" class="govuk-service-navigation__toggle govuk-js-service-navigation-toggle" aria-controls="navigation" hidden aria-hidden="true">
+            Menu
+          </button>
+
+          <ul class="govuk-service-navigation__list" id="navigation" >
+
+            
+              
+              <li class="govuk-service-navigation__item govuk-service-navigation__item--active">
+                  <a class="govuk-service-navigation__link" href="#/1" aria-current="true">
+                                    
+                  <strong class="govuk-service-navigation__active-fallback">Navigation item 1</strong>
+
+                  </a>
+              </li>
+
+              
+              <li class="govuk-service-navigation__item">
+                  <a class="govuk-service-navigation__link" href="#/2">
+                                    
+Navigation item 2
+                  </a>
+              </li>
+
+              
+              <li class="govuk-service-navigation__item">
+                  <a class="govuk-service-navigation__link" href="#/3">
+                                    
+Navigation item 3
+                  </a>
+              </li>
+
+            </ul>
+        </nav>
+
+      
+    </div>
+
+    <nav class="govuk-language-switcher" aria-label="Language switcher">
+  <ul class="govuk-language-switcher__list">
+    <li class="govuk-language-switcher__list-item">
+      <span class="govuk-language-switcher__text" lang="en" aria-current="true">English</span>
+    </li>
+    <li class="govuk-language-switcher__list-item">
+      <a class="govuk-language-switcher__link" href="#/cy" lang="cy" hreflang="cy" rel="alternate">Cymraeg</a>
+    </li>
+  </ul>
+</nav></div>
+
+  </section>

Action run for fce53c6

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

Other changes to npm package

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 65007adb1..685a94638 100644
--- a/packages/govuk-frontend/dist/govuk/components/service-navigation/_mixin.scss
+++ b/packages/govuk-frontend/dist/govuk/components/service-navigation/_mixin.scss
@@ -106,12 +106,7 @@
 
   .govuk-service-navigation__service-name {
     @include base.govuk-typography-weight-bold;
-  }
-
-  // Annoyingly this requires a compound selector in order to overcome the
-  // specificity of the other link colour override we're doing
-  .govuk-service-navigation__service-name .govuk-service-navigation__link {
-    @include base.govuk-link-style-text;
+    @include base.govuk-link-theme-text;
   }
 
   // Allow navigation section to always take up maximum available space,
@@ -226,6 +221,11 @@
 
     --govuk-text-colour: #{base.govuk-functional-colour(inverse-text)};
     background-color: base.govuk-functional-colour(brand);
+    // Avoid defining specific link styles when inverted and just use the text colours
+    @include base.govuk-link-theme-text;
+
+    // Similarly to the Breadcrumbs, switch the secondary text colour to `currentcolor`
+    --govuk-secondary-text-colour: currentcolor;
 
     .govuk-width-container {
       border-width: 1px 0;
@@ -244,11 +244,6 @@
       border-color: currentcolor;
     }
 
-    // Override link styles
-    .govuk-service-navigation__link {
-      @include base.govuk-link-style-text;
-    }
-
     // Override mobile menu toggle colour when not focused
     .govuk-service-navigation__toggle:not(:focus) {
       color: currentcolor;
diff --git a/packages/govuk-frontend/dist/govuk/components/service-navigation/fixtures.json b/packages/govuk-frontend/dist/govuk/components/service-navigation/fixtures.json
index cdf49259c..2bf0a5855 100644
--- a/packages/govuk-frontend/dist/govuk/components/service-navigation/fixtures.json
+++ b/packages/govuk-frontend/dist/govuk/components/service-navigation/fixtures.json
@@ -694,6 +694,39 @@
             "screenshot": false,
             "html": "<section aria-label=\"Service information\" class=\"govuk-service-navigation\"\ndata-module=\"govuk-service-navigation\"\n>\n      <div class=\"govuk-width-container\">\n\n    <div class=\"govuk-service-navigation__container\">\n      \n        <span class=\"govuk-service-navigation__service-name\">\n            <span class=\"govuk-service-navigation__text\">Service name</span>\n        </span>\n\n      \n        <nav aria-label=\"Menu\" class=\"govuk-service-navigation__wrapper\">\n          <button type=\"button\" class=\"govuk-service-navigation__toggle govuk-js-service-navigation-toggle\" aria-controls=\"navigation\" hidden aria-hidden=\"true\">\n            Menu\n          </button>\n\n          <ul class=\"govuk-service-navigation__list\" id=\"navigation\" >\n\n            \n              \n              <li class=\"govuk-service-navigation__item govuk-service-navigation__item--active\">\n                  <a class=\"govuk-service-navigation__link\" href=\"#/1\" aria-current=\"true\">\n                                    \n                  <strong class=\"govuk-service-navigation__active-fallback\">Navigation item 1</strong>\n\n                  </a>\n              </li>\n\n              \n              <li class=\"govuk-service-navigation__item\">\n                  <a class=\"govuk-service-navigation__link\" href=\"#/2\">\n                                    \nNavigation item 2\n                  </a>\n              </li>\n\n              \n              <li class=\"govuk-service-navigation__item\">\n                  <a class=\"govuk-service-navigation__link\" href=\"#/3\">\n                                    \nNavigation item 3\n                  </a>\n              </li>\n\n            </ul>\n        </nav>\n\n      \n        <div class=\"govuk-service-navigation__end\">\n          <nav class=\"govuk-language-switcher\" aria-label=\"Language switcher\">\n  <ul class=\"govuk-language-switcher__list\">\n    <li class=\"govuk-language-switcher__list-item\">\n      <span class=\"govuk-language-switcher__text\" lang=\"en\" aria-current=\"true\">English</span>\n    </li>\n    <li class=\"govuk-language-switcher__list-item\">\n      <a class=\"govuk-language-switcher__link\" href=\"#/cy\" lang=\"cy\" hreflang=\"cy\" rel=\"alternate\">Cymraeg</a>\n    </li>\n  </ul>\n</nav>\n        </div>\n    </div>\n\n    </div>\n\n  </section>"
         },
+        {
+            "name": "with language switcher, inverse",
+            "options": {
+                "classes": "govuk-service-navigation--inverse",
+                "serviceName": "Service name",
+                "serviceUrl": "#/",
+                "navigation": [
+                    {
+                        "href": "#/1",
+                        "text": "Navigation item 1",
+                        "active": true
+                    },
+                    {
+                        "href": "#/2",
+                        "text": "Navigation item 2"
+                    },
+                    {
+                        "href": "#/3",
+                        "text": "Navigation item 3"
+                    }
+                ],
+                "slots": {
+                    "end": "<nav class=\"govuk-language-switcher\" aria-label=\"Language switcher\">\n  <ul class=\"govuk-language-switcher__list\">\n    <li class=\"govuk-language-switcher__list-item\">\n      <span class=\"govuk-language-switcher__text\" lang=\"en\" aria-current=\"true\">English</span>\n    </li>\n    <li class=\"govuk-language-switcher__list-item\">\n      <a class=\"govuk-language-switcher__link\" href=\"#/cy\" lang=\"cy\" hreflang=\"cy\" rel=\"alternate\">Cymraeg</a>\n    </li>\n  </ul>\n</nav>"
+                }
+            },
+            "hidden": false,
+            "description": "HTML rendered by the language switcher component can be injected into the `end` slot of the service header.",
+            "pageTemplateOptions": {
+                "bodyClasses": "app-template__body--inverse"
+            },
+            "screenshot": false,
+            "html": "<section aria-label=\"Service information\" class=\"govuk-service-navigation govuk-service-navigation--inverse\"\ndata-module=\"govuk-service-navigation\"\n>\n      <div class=\"govuk-width-container\">\n\n    <div class=\"govuk-service-navigation__container\">\n      \n        <span class=\"govuk-service-navigation__service-name\">\n            <a href=\"#/\" class=\"govuk-service-navigation__link\">\n              Service name\n            </a>\n        </span>\n\n      \n        <nav aria-label=\"Menu\" class=\"govuk-service-navigation__wrapper\">\n          <button type=\"button\" class=\"govuk-service-navigation__toggle govuk-js-service-navigation-toggle\" aria-controls=\"navigation\" hidden aria-hidden=\"true\">\n            Menu\n          </button>\n\n          <ul class=\"govuk-service-navigation__list\" id=\"navigation\" >\n\n            \n              \n              <li class=\"govuk-service-navigation__item govuk-service-navigation__item--active\">\n                  <a class=\"govuk-service-navigation__link\" href=\"#/1\" aria-current=\"true\">\n                                    \n                  <strong class=\"govuk-service-navigation__active-fallback\">Navigation item 1</strong>\n\n                  </a>\n              </li>\n\n              \n              <li class=\"govuk-service-navigation__item\">\n                  <a class=\"govuk-service-navigation__link\" href=\"#/2\">\n                                    \nNavigation item 2\n                  </a>\n              </li>\n\n              \n              <li class=\"govuk-service-navigation__item\">\n                  <a class=\"govuk-service-navigation__link\" href=\"#/3\">\n                                    \nNavigation item 3\n                  </a>\n              </li>\n\n            </ul>\n        </nav>\n\n      \n    </div>\n\n    <nav class=\"govuk-language-switcher\" aria-label=\"Language switcher\">\n  <ul class=\"govuk-language-switcher__list\">\n    <li class=\"govuk-language-switcher__list-item\">\n      <span class=\"govuk-language-switcher__text\" lang=\"en\" aria-current=\"true\">English</span>\n    </li>\n    <li class=\"govuk-language-switcher__list-item\">\n      <a class=\"govuk-language-switcher__link\" href=\"#/cy\" lang=\"cy\" hreflang=\"cy\" rel=\"alternate\">Cymraeg</a>\n    </li>\n  </ul>\n</nav></div>\n\n  </section>"
+        },
         {
             "name": "with slotted content",
             "options": {
diff --git a/packages/govuk-frontend/dist/govuk/helpers/_links.scss b/packages/govuk-frontend/dist/govuk/helpers/_links.scss
index 2cc324f91..4cc56e74a 100644
--- a/packages/govuk-frontend/dist/govuk/helpers/_links.scss
+++ b/packages/govuk-frontend/dist/govuk/helpers/_links.scss
@@ -121,24 +121,11 @@
 /// @access public
 
 @mixin govuk-link-style-error {
-  &:link,
-  &:visited {
-    color: govuk-functional-colour(error);
-  }
-
-  &:hover {
-    color: govuk-colour("red", $variant: "shade-50");
-  }
+  @include govuk-link-style-no-visited-state;
 
-  &:active {
-    color: govuk-functional-colour(error);
-  }
-
-  // When focussed, the text colour needs to be darker to ensure that colour
-  // contrast is still acceptable
-  &:focus {
-    color: govuk-functional-colour(focus-text);
-  }
+  --govuk-link-colour: #{govuk-functional-colour(error)};
+  --govuk-link-hover-colour: #{govuk-colour("red", $variant: "shade-50")};
+  --govuk-link-active-colour: var(--govuk-link-colour);
 }
 
 /// Success link styles
@@ -158,24 +145,15 @@
 /// @access public
 
 @mixin govuk-link-style-success {
-  &:link,
-  &:visited {
-    color: govuk-functional-colour(success);
-  }
-
-  &:hover {
-    color: govuk-colour("green", $variant: "shade-50");
-  }
-
-  &:active {
-    color: govuk-functional-colour(success);
-  }
-
-  // When focussed, the text colour needs to be darker to ensure that colour
-  // contrast is still acceptable
-  &:focus {
-    color: govuk-functional-colour(focus-text);
-  }
+  // In order to avoid a breaking change, we need the selectors
+  // to be output by the mixin, as sometimes the mixins are used on their own
+  // and not all instances use `govuk-link-common`
+  @include govuk-link-style-default;
+  @include govuk-link-theme-no-visited-state;
+
+  --govuk-link-colour: #{govuk-functional-colour(success)};
+  --govuk-link-hover-colour: #{govuk-colour("green", $variant: "shade-50")};
+  --govuk-link-active-colour: var(--govuk-link-colour);
 }
 
 /// Muted link styles
@@ -195,21 +173,15 @@
 /// @access public
 
 @mixin govuk-link-style-muted {
-  &:link,
-  &:visited {
-    color: govuk-functional-colour(secondary-text);
-  }
-
-  &:hover,
-  &:active {
-    color: govuk-functional-colour(text);
-  }
-
-  // When focussed, the text colour needs to be darker to ensure that colour
-  // contrast is still acceptable
-  &:focus {
-    color: govuk-functional-colour(focus-text);
-  }
+  // In order to avoid a breaking change, we need the selectors
+  // to be output by the mixin, as sometimes the mixins are used on their own
+  // and not all instances use `govuk-link-common`
+  @include govuk-link-style-default;
+  @include govuk-link-theme-no-visited-state;
+
+  --govuk-link-colour: #{govuk-functional-colour(secondary-text)};
+  --govuk-link-hover-colour: #{govuk-functional-colour(text)};
+  --govuk-link-active-colour: var(--govuk-link-hover-colour);
 }
 
 /// Text link styles
@@ -229,15 +201,26 @@
 /// @access public
 
 @mixin govuk-link-style-text {
-  &:link,
-  &:visited,
-  &:active {
-    color: govuk-functional-colour(text);
-  }
+  // In order to avoid a breaking change, we need the selectors
+  // to be output by the mixin, as sometimes the mixins are used on their own
+  // and not all instances use `govuk-link-common`
+  @include govuk-link-style-default;
+  @include govuk-link-theme-text;
+}
 
-  &:focus {
-    color: govuk-functional-colour(focus-text);
-  }
+/// Themes links to look like the text
+///
+/// Applies the appropriate `--govuk-link-...` custom properties
+/// so that the styled element or links inside the style elements
+/// look like the text
+///
+/// @access private
+@mixin govuk-link-theme-text {
+  @include govuk-link-theme-no-visited-state;
+
+  --govuk-link-colour: #{govuk-functional-colour(text)};
+  --govuk-link-hover-colour: var(--govuk-link-colour);
+  --govuk-link-active-colour: var(--govuk-link-colour);
 }
 
 /// Inverse link styles
@@ -257,14 +240,15 @@
 /// @access public
 
 @mixin govuk-link-style-inverse {
-  &:link,
-  &:visited {
-    color: govuk-functional-colour(inverse-text);
-  }
-
-  &:focus {
-    color: govuk-functional-colour(focus-text);
-  }
+  // In order to avoid a breaking change, we need the selectors
+  // to be output by the mixin, as sometimes the mixins are used on their own
+  // and not all instances use `govuk-link-common`
+  @include govuk-link-style-default;
+  @include govuk-link-theme-no-visited-state;
+
+  --govuk-link-colour: #{govuk-functional-colour(inverse-text)};
+  --govuk-link-hover-colour: var(--govuk-link-colour);
+  --govuk-link-active-colour: var(--govuk-link-colour);
 }
 
 /// Default link styles, without a visited state
@@ -288,27 +272,23 @@
 /// @access public
 
 @mixin govuk-link-style-no-visited-state {
-  &:link {
-    color: govuk-functional-colour(link);
-  }
+  // In order to avoid a breaking change, we need the selectors
+  // to be output by the mixin, as sometimes the mixins are used on their own
+  // and not all instances use `govuk-link-common`
+  @include govuk-link-style-default;
 
-  &:visited {
-    color: govuk-functional-colour(link);
-  }
-
-  &:hover {
-    color: govuk-functional-colour(link-hover);
-  }
-
-  &:active {
-    color: govuk-functional-colour(link-active);
-  }
+  @include govuk-link-theme-no-visited-state;
+}
 
-  // When focussed, the text colour needs to be darker to ensure that colour
-  // contrast is still acceptable
-  &:focus {
-    color: govuk-functional-colour(focus-text);
-  }
+/// Themes links to have no visited states
+///
+/// Applies the appropriate `--govuk-link-...` custom properties
+/// so that the styled element or links inside the style elements
+/// have no visited states
+///
+/// @access private
+@mixin govuk-link-theme-no-visited-state {
+  --govuk-link-visited-colour: var(--govuk-link-colour);
 }
 
 /// Remove underline from links

Action run for fce53c6

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

📋 Stats

File sizes

File Size Percentage change
dist/govuk-frontend-development.min.css 129.86 KiB 4.3%
packages/govuk-frontend/dist/govuk/govuk-frontend.min.css 129.84 KiB 4.3%

No changes to module sizes.


Action run for fce53c6

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.

Update links to use custom properties to implement their different styles

2 participants