Skip to content

Conversation

@maureenlholland
Copy link
Collaborator

@maureenlholland maureenlholland commented Apr 16, 2025

Description

This is more a thought experiment than anything at this stage. The idea is to reduce base element link specificity so it is easily overridden by a single class

Base element specificity: 0 0 1
not visited
Screenshot 2025-04-16 at 12 33 56 PM

visited
Screenshot 2025-04-16 at 12 32 03 PM

Class specificity: 0 1 0

NOTE: the danger of custom properties is that they inherit all the way down the tree, so could be going further than desired if "donut scope" was intended

  • I have documented this change in the design system.
  • I have recorded this change in CHANGELOG.md.

Issue

Add a link to a related GitHub issue if applicable.

Testing

Enter helpful notes for whoever code reviews this change.

This keeps all base element styles at a single element level of
specificity and ensures any class will overrule the styles
@maureenlholland maureenlholland force-pushed the reduce-base-link-style-specificity branch from 4de77cb to 8931907 Compare April 16, 2025 10:57
@maureenlholland maureenlholland added the Discussion 🚲 Something we need to discuss label Apr 16, 2025
@maureenlholland
Copy link
Collaborator Author

Hoping this direction is in line with transition to CSS variables

@maureenlholland maureenlholland force-pushed the reduce-base-link-style-specificity branch from 8931907 to 1dd40e8 Compare April 16, 2025 11:17
:link {
color: $link-color;
a {
color: var(--link-color);
Copy link
Collaborator Author

@maureenlholland maureenlholland Apr 16, 2025

Choose a reason for hiding this comment

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

This was interesting. I thought I could redeclare the --link-color var within the :visited selector scope, but it didn't seem to take effect as computed style (tested with and without the :where() selector). Dev tools seems to show both the correct variable and the wrong computed style 🤷‍♀️

:root {
  --link-color: default-color;
  --link-color-visited: visited-color;
}

a {
  color: var(--link-color);
}

a:where(:visited) {
  --link-color: var(--link-color-visited);
}

Copy link
Collaborator Author

@maureenlholland maureenlholland Apr 16, 2025

Choose a reason for hiding this comment

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

pseudo-class variable correctly identified
computed value still default color
Screenshot 2025-04-16 at 12 13 36 PM

root inheritance correctly overruled
Screenshot 2025-04-16 at 12 14 14 PM
^unrelated, looks like we have a mixed up value declaration on :root, visited hover color should be #952bb9

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm worried about using :where since the support for it is less wide spread than support for dynamic properties. Can you help me understand what would happen if a visitor had variable support but not where support? Links are visible they just don't get hover/focus? Or something more dire?

Copy link
Contributor

Choose a reason for hiding this comment

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

I looked into CSS layers as a possible solution (I am so excited for these!) but the support for them is about the same as support for :where()

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

When custom properties are supported but :where() is not, browsers will ignore any :where() selectors, so you get default user agent styles for hover/focus etc.
does-not-support-where

vs. supported and applying the :where() rules
supports-where

Copy link
Contributor

Choose a reason for hiding this comment

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

So on a background set to black with a CSS variable they will get blue/purple links?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Will check tomorrow. I think they will get the --link-coloror --link-color-visited value. If we use mzp-t-dark for dark background, the links will still have good contrast (those variables will respond to the dark theme), but their color will not change on hover/focus.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ok, so older browser links would always get the --link-color regardless of state or :visited status (all those rules are ignored). The correct link color will apply according to light/dark theme.

states visited


// any descendant link will inherit dark theming (replace light-links mixin)
// this might belong in root file? site-wide dark theme class
.mzp-t-dark {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

inherited values
Screenshot 2025-04-16 at 12 35 52 PM


// any descendant link will inherit white theming (replace white-links mixin)
// link-specific theme class
.mzp-t-white-links {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Inherited values
Screenshot 2025-04-16 at 12 37 39 PM

Copy link
Contributor

Choose a reason for hiding this comment

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

I love the move the variables here! We need to be careful we also move to variables for the background colours at the same time so we don't get in a circumstance where the background is defined as dark using a hex colour but the component depends on variables for the links.

Perhaps the prudent thing to do is to bundle the CSS variable changes into one big point release version that we test on un-supporting browsers.

&:active {
color: var(--link-color-visited-hover);
}
&:where(:hover, :focus, :active) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

wiping out specificity of stateful pseudo-classes with :where()
Screenshot 2025-04-16 at 12 37 28 PM

Copy link
Contributor

@stephaniehobson stephaniehobson May 12, 2025

Choose a reason for hiding this comment

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

Another thing we could do to make styling links easier would be to drop the separate styles for visited links on hover/focus/active and rely on source order for the override.

  1. Link
  2. Visited
  3. Hover
  4. Active
  5. Focus

Copy link
Contributor

@stephaniehobson stephaniehobson May 12, 2025

Choose a reason for hiding this comment

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

I've always found the a:link:visited:hover edge case to be confusing.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Will add a commit with source order for comparison

Copy link
Collaborator Author

@maureenlholland maureenlholland May 13, 2025

Choose a reason for hiding this comment

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

The only potential conflict here is that a stateful pseudo-class + element selector will beat a component class selector (if it doesn't also explicitly override the state). This is most likely to happen with button component and link-specific visited state.
visited-button-link

We could unnest the link states so they are only 0,1,0 specificity and rely on source order to override (basic element styles are overruled by component styles)

Or we could also include the :visited state selector on any components that could also be links under the hood 0,2,0 beats 0,1,1

Copy link
Contributor

@janbrasna janbrasna left a comment

Choose a reason for hiding this comment

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

This makes sense, looks elegant, and is easy to follow/understand. (Redefining the vars for states as shown above resulting in the rendering woes looks cool at first, but this take looks more reliable.)

The tricky part might be following the themes to the actual components, to make sure it either still works as intended, or what tweaks could be useful there… as it's usually the components being consumed and redefined downstream causing the specificity issues, e.g. the Modal: (#758)

.mzp-c-modal-overlay-contents {
@include text-body-lg;
@include light-links;

Comment on lines 47 to 52
// this might belong in a different CTA file?
// it's not meant to apply to `a` elements unless they have the class added directly
// the other classes in this file are theme containers that select `a` elements inside them
.mzp-c-cta-link {
font-family: $button-font-family;
font-family: var(--button-font-family);
font-weight: bold;
Copy link
Contributor

Choose a reason for hiding this comment

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

"this might belong in a different CTA file?"

IIUC the organization of files match the grouping of component pages, so this chubby "CTA link" styling seems to belong with these, as it's one of the variants or states of the Links styles:

https://protocol.mozilla.org/components/detail/links--call-to-action

(It feels like it's listed as a flavor of a base element, not treated as a component per se — to be used mostly in other components…)

Copy link
Contributor

@janbrasna janbrasna left a comment

Choose a reason for hiding this comment

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

These should also no longer complain:


.mzp-c-button, /* stylelint-disable-line no-duplicate-selectors */
a.mzp-c-button {
.mzp-c-button /* stylelint-disable-line no-duplicate-selectors */ {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
.mzp-c-button /* stylelint-disable-line no-duplicate-selectors */ {
.mzp-c-button {

// * -------------------------------------------------------------------------- */
// Primary

/* stylelint-disable-next-line no-duplicate-selectors */
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/* stylelint-disable-next-line no-duplicate-selectors */

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We do still have a duplicate selector here (there's no mzp-t-primary theme class to distinguish this selector from the other default button styles), but this is the only place we need the disable comment now

duplicate-selector

@maureenlholland maureenlholland force-pushed the reduce-base-link-style-specificity branch from dee5f6e to 9de9e96 Compare April 28, 2025 15:37
background-color: transparent;
border-color: $color-black;
color: $color-black;
.mzp-c-button.mzp-t-secondary {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is so much nicer, I want this to work 😭

--link-color-inverse: #{tokens.$color-blue-10};
--link-color-visited-hover-inverse: #{tokens.$color-violet-05};
--link-color-visited-hover: #{tokens.$color-link-hover};
--link-color-visited-hover: #{tokens.$color-link-visited-hover};
Copy link
Contributor

Choose a reason for hiding this comment

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

Another place that would be simplified by having the same hover state for both visited and unvisited links.

@maureenlholland maureenlholland force-pushed the reduce-base-link-style-specificity branch from bce794b to 97cc65c Compare May 13, 2025 10:54

&:hover, &:active, &:focus {
/* stylelint-disable-next-line color-function-notation */
color: var(--link-color-with-state, hsl(from var(--link-color) h s calc(l - 10)));
Copy link
Collaborator Author

@maureenlholland maureenlholland May 13, 2025

Choose a reason for hiding this comment

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

Demo of functional hover color approach:

  1. By default, we automatically darken the lightness of the color by 10%
  2. We can override this default with another function (i.e. dark theme automatically lightens color) or another set value (i.e. white theme uses white)

relative colors with light & dark hover
light
dark

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We could also do this with sass color.scale for better support but we have to use a sass variable as the input argument (CSS custom properties are not available as values at build time)
need-sass-variable

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This would allow us to drop support for hover tokens and (hopefully) simplify maintenance

@maureenlholland maureenlholland added Do Not Merge ⚠️ Do Not Merge and removed Discussion 🚲 Something we need to discuss labels Jul 9, 2025
@maureenlholland
Copy link
Collaborator Author

After internal discussion, we want to drop support for :hover:visited style and rely on order of appearance to ensure :hover style always applied on hover state (whether link is visited or not)

Do Not Merge until Icon release is published

@stephaniehobson
Copy link
Contributor

Closing in favour of #1077

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants