CSS can help improve your HTML⁉ - Ep 2: buttons and links.

CSS can help improve your HTML⁉ - Ep 2: buttons and links.

Part 2 of using CSS to make our HTML better!

In this second part of this series we will be exploring how we can use CSS to ensure (and enforce) that we are using <button> and <a> elements correctly...you would be surprised how many developers get this wrong!

If you missed part one you may need to have a quick read of that so you get the key concepts as I won't be repeating them in detail in this article:

Read part 1

All caught up? Great!

Before we start part 2, it is worth mentioning (reiterating) that this concept relies on two style sheets.

One for production (and development) and one for development only (the one containing error messages).

In the fiddles I have indicated which styles you would use in your production stylesheet and which to use in your development stylesheet!

Right, with that reminder out of the way, let's begin!

A button is a button!

I know I keep saying it and I sound like a broken record but, a button is a button!

See, I told you I keep saying it!

But what if we could guide our developers towards this logical conclusion using CSS?

Well first thing is first, we can make it so our .btn class only works on a <button>.

button.btn{}

Try and use the .btn class on anything else and it just won't work!

but we can do better than that!

What if we can also give our team a little bit of a warning to let them know they have used the wrong element?

.btn:not(button){/* wrong element error highlighting */}

Meaningful warning and error messages

Now while adding outlines to stuff that is wrong is great, it isn't very informative.

Luckily we can fix that also!

By using :after we can insert an error message

So we end up with a selector to add an outline and another selector to add a warning that is the same but using the :after pseudo selector.

By utilising the content property we can add a custom error message!

.btn:not(button) /* outlines */
.btn:not(button):after /* error message */

Example for buttons

In this example we have a real button and a fake button made with a <div>.

Notice how the real button works and the fake button has no styling as well as an error message!

Taking it one step further!

I would argue that for most sites we can actually go one step further and completely do away with the .btn class on our buttons!

This helps with consistency, looks neater and enforces our policy of <button>s for buttons!

At this point we no longer even need an error check as we can only get styling on a <button>!

We can do this by using the element as the selector button{}!

button{
    padding: 0.75rem;
    background-color: #333;
    color: #fff;
    margin: 2rem 0;
    border: 0;
    border-radius: 0.3rem;
    width: 100%;
}

However this may have one unintended consequence...

That's great, but we often style anchors like buttons!

And that is a valid point and nowadays, you are right!

However we can recycle our selector from the first part so we can have a .btn class only on anchors (<a>)!

button,
a.btn{/* our styles for buttons! */}

This does mean we have to reintroduce our error checks though, but this time to ensure that we only use the .btn class on an anchor!

This raises an interesting question, do we want an error to show if we have a .btn class on a <button>?

If we do then our error selector just needs a subtle change

.btn:not(a){}
.btn:not(a):after{}

This will add our error messages to <button class="btn", which might be confusing.

As it won't do any harm having the .btn class on a <button> (as we have no valid selector for it anyway) I would think it better to not show error messages in this instance.

Yet again the power of CSS makes this relatively simple, we can add multiple items within our :not selector!

.btn:not(a,button){}
.btn:not(a,button):after{}

This is saying:-

  • select all items with the .btn class
  • that are not (:not())
  • an anchor (a)
  • OR a button ,button (comma seperated).

Example including anchors

Bringing everything together we end up with the following:

Looks great, but we can go even further!

But we only want valid anchors

Now that we have allowed the .btn class to be added to anchors we have opened up another problem!

The dreaded <a href="#" or <a href="javascript: void()!

This anti-pattern has not been needed since HTML5 (you did read my rant on buttons I linked earlier didn't you?).

So how can we fix this one?

Well in part 1 we used attribute selectors [attr] and once again they can come to our rescue!

As this is a site wide error (it should never be used!) we can ignore the .btn selector and just focus on anchors!

The first one is easier to solve, we select any anchors with a href="#" (and while we are at it an empty href or missing href too!)

a:not([href]), /* A missing href attribute! */
a[href=""], /* an empty (or null) href */
a[href="#"] /* the anti pattern! */

That covers most of them, we just need to highlight the JavaScript anti-pattern!

Yet again we covered this in part 1, we can use the "begins with" notation:

a[href^="javascript:" i]

Remember the "i" at the end to make it case insensitive.

We used the "begins with" ^= syntax for this example as it doesn't have to be javascript: void(), this way we can capture all JavaScript hrefs no matter what function they call!

Example for valid anchors!

Utilising the same system of using :after we can add messages to our anchors as well!

Note: We can make this much better by having separate message depending on whether the problem is a missing href, bad href or the use of JavaScript. I haven't done that in the example but you would do it as follows:

a:not([href]):after{content: "missing href attr"}
a[href="#"]:after{content: "anti-pattern on href detected"}
a[href=""]:after{content: "empty or null href"}
a[href^="javascript:" i]:after{content: "using javascript in href anti-pattern detected"}

Additional things to mention

As pointed out by @ashleyjsheridan it is perfectly valid to have an anchor without a href.

As such where I marked anchors with missing or empty href attributes as errors this was a mistake on my part.

Instead these should be downgraded to "warnings" just to pick up on mistakes.

Additionally this then means that if you use <a name="sectionName"> for whatever reason it will only be a warning (although this is quite a dated way to do things as you can just give an element an id attribute and link to that instead).

Overall it has no bearing though as the development style sheet will not impact production, might just be a little annoying though if you do edge case stuff like dynamically adding a href to an existing anchor.

Additionally they mention:

Might also be worth considering the 3 button types: button, submit, and reset.

This is an excellent point, so to add those you would update the selector to target:

button,
a.btn,
input[type="button"],
input[type="submit"],
input[type="reset"]{
  /* can use the same default styling and overwrite with additional styles to differentiate as needed */
}

If you need to use those.

Conclusion

A lot of what we learned in part 1 has carried over to part 2.

It is just that our selectors have become a little more complex.

But hopefully you can see how this allows us to enforce business style guides once again (having or not requiring a .btn class on <button> elements for example).

In the next part we are going to focus on document structure best practices and introduce some WAI-ARIA attributes and some tips and tricks with those!

Thanks for Reading!

Thanks for reading, I hope you enjoyed it!

In fact to show my appreciation please do have a virtual ❤ and 🦄 to show how much it means to me!

If you enjoyed this and want something just a little (OK a lot longer I have written) why not read my monster accessibility tips article:

Read my 17500+ word monster article with 101 accessibility tips

Have a great week!