Inheritance, visited links and colour-contrast

rossangus

Ross Angus

Posted on January 2, 2020

Inheritance, visited links and colour-contrast

As I'm sure you know, by default, links have a bright blue colour and visited links have a maroon colour. Look!

While you might not be in love with that colour scheme, users tend to associate blue with I've not been here before and purple with I've already seen this page. This is a useful convention to piggy-back off, in order for users to understand your UI faster.

But how do those text colours work against different background colours?

Not ideal. According to WebAIM's colo(u)r contrast checker, this has a ratio of 2.44:1, which is a fail for AA and AAA.

Our usual pattern for working around this is to use contextual selectors:

.bg-black a:link    {color: #99f;}
.bg-black a:visited {color: #fcf;}
Enter fullscreen mode Exit fullscreen mode

This mostly works, until you find you need to nest one coloured box inside another:

You could try and get out of this mess with contextual selectors, I suppose:

.bg-black a:link             {color: #99f;}
.bg-black a:visited          {color: #fcf;}
.bg-black .bg-pink a:link    {color: #00e;}
.bg-black .bg-pink a:visited {color: #551a8b;}
Enter fullscreen mode Exit fullscreen mode

... but anticipating every potential markup configuration in your code is hardly a route to a small CSS file.

Situations such as this is the reason why the property inherit was invented. When I first read about the inherit property, I thought I can't imagine ever having to use that. Inheriting the properties of the parent? Browsers do that sensibly without any input from us.

Here's one way to do it:

.bg-black {
  background: black;
  color: white;
}
.bg-pink {
  background: fuchsia;
  color: black;
}
[class*="bg-"] a {
  color: inherit;
}
Enter fullscreen mode Exit fullscreen mode

The final rule above would allow anchor tags to inherit the foreground colour of any element they were inside, which had a class which included the string bg- anywhere in its name.

Inheriting an unwanted gift

Here's a little quirk of this behaviour: if you want a property to be available to the child, you need to define it on the parent, even if it isn't used. Lemmy explain that a bit.

Say you didn't want to use with text-decoration to underline your anchors, but wanted to use border-bottom instead. But you also wanted the border-color to be inherited by the kids. You'd need to do something like this:

a {
  border-bottom: solid .1em blue;
  text-decoration: none;
}
a:visited {
  border-bottom-color: purple;
}
.bg-black {
  /* Not used directly on this element... */
  border-color: white;
  color: white;
}
[class*="bg-"] a {
  color: inherit;
  /* ... but inherited here. */
  border-color: inherit;
}
Enter fullscreen mode Exit fullscreen mode

Note that you can only use the inherit value on its own. The following is not valid CSS:

/* Oh God, don't do this */
[class*="bg-"] a {
  border-bottom: dashed .1em inherit;
}
Enter fullscreen mode Exit fullscreen mode

As long as you don't mind your links being the same colour as your body text, this is job done. This is exactly what inherit was invented for.

A brief visit to :visited links

inherit isn't a solution to our problem yet, but let me swerve slightly here, because I find the following digression interesting (your milage may vary).

When we attempt to style :visited links, there's a white-list of CSS properties we're allowed to use. The reason for this is solid: if we allow the browser to start mucking about with :before and :after elements and absolute positioning, pretty quickly some wag would find a way to put one million hidden links at the bottom of their page and work out which sites you've already visited on the web.

Those CSS properties which are good to use only work when they've already been applied to the default anchor style. For example, this CSS would do something:

a:link {outline: solid .1em blue;}
a:visited {outline-color: purple;}
Enter fullscreen mode Exit fullscreen mode

... where as this would not:

a:link {border-bottom: solid .1em blue;}
a:visited {
  outline: solid .1em purple;
  outline-color: purple;
}
Enter fullscreen mode Exit fullscreen mode

In the above example, because no outline is defined on links by default, no outline will appear on the visited link either.

I originally had the idea that visited links could have an opacity of 0.8, so they would work against any background colour, as long as they inherited the text colour. But as opacity isn't on the list of permitted declarations for :visited links, it wouldn't work.

So how do we get visited links recoloured against any background?

Let's recap what a perfect answer to this question would have to achieve:

  • Links will always display a colour tint which shows them as being either visited or not visited
  • Link colours should have sufficient contrast with the background colour
  • Coloured boxes should be able to be nested together in any order and the links should still maintain contrast
  • This should not require any classes on the links themselves

My answer to this mostly works, but unfortunately requires that any coloured boxes must be an immediate child of its parent. Here's a couple of DOM trees, to illustrate:

This is the tricky piece of CSS which does the hard work:

/* Assuming .bg-1 is a dark colour... */
.bg-1 > :not([class*="bg-"]) a:link {
  color: #9ff; /* A light blue */
}
.bg-1 > :not([class*="bg-"]) a:visited {
  color: #fcf; /* A light pink */
}
Enter fullscreen mode Exit fullscreen mode

If I were to write that first selector in English, I'd say Get me all of the links which sit under an element with the class bg-1, but not if they're a child of any other element which has a class which contains bg-.

You might be wondering why I used the immediate child selector (>). Because a space in a selector could be interpeted by the browser as immediate child or child twenty levels deep in the DOM, there's a means for the browser's selecter engine to sidestep that :not([class*="bg-"]) part, and to still select the wrong links.

If you imagine that the selector was less rigid, say:

/* Note: this doesn't work */
.bg-1 :not([class*="bg-"]) a:link {
  color: #9ff; /* A light blue */
}
Enter fullscreen mode Exit fullscreen mode

This selector will still match links inside .bg-2 elements (which themselves are inside .bg-1 elements) because it can skip over the .bg-2 part of the DOM chain, and just substitute the paragraph tag in instead. Look:

Trying to explain the above paragraph in an image, but still failing

This happens because if the CSS rule which controls link colour appears later in the style sheet, it will beat the earlier rule for inheritance.

Stress test

What I'm sure you're curious about is how can this be tested, in the most garish possible way. Got you covered there, buddy:

This article was originally published on CodePen.

💖 💪 🙅 🚩
rossangus
Ross Angus

Posted on January 2, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related